目录
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。
在设计三方接口调用的方案时候,需要考虑到安全性和可用性。以下是一种设计方案概述,其中包括使用API 秘钥(Access Key / Secret Key) 进行身份验证和设置回调地址
设计方案概述
1、API密钥生成: 为每个三方应用生成唯一的API 密钥对(AK /SK), 其中AK 用于标识应用,SK应用进行签名和加密。
AK: Access Key Id 用于标识用户
SK: Secret Access Key 是用于用于加密认真字符串和用来验证字符串的秘钥,其中SK 必须加密
通过使用Access Key Id/ Secret Access Key 加密方凡来验证某个请求的发送者身份
2、接口鉴权: 在进行接口调用的时候,客户端需要使用AK和请求参数生成签名,并将其放入请求头或参数中以进行身份验证。
3、回调地址设置: 三方应用提供回调地址,用于接收异步同步和回调结果
4、接口API设计: 设计接口的URL、HTTP方法、请求参数、响应格式等细节。
权限划分
appID : 应用的唯一标识
用来标识你的开发者账号的,即: 用户id,可以在数据库添加索引,方便快速查找,同一个appId 可以对应多个 appKey + appSecret 达到权限的
appKey 公钥(相当于账号)
公开的,调用服务所需要的秘钥,是用户的身份认证标识,用于调用凭条可用服务,可以简单理解是账号
appSecret: 私钥(相当于密码)
签名的秘钥,是跟appKey配套使用的,可以简单理解是密码
token: 令牌(过期失效)
使用方法
① 向第三方服务器请求授权的时候,带上appKey 和 appSecret (需要存在服务端)
② 第三方服务器验证的appKey 和appSecret 在数据库、缓存中有没有记录
③ 如果有,生成一串唯一的字符串(token) ,返回给服务器,服务器再返回给客户端。
④ 后续客户端每次请求都需要带上token 令牌
为什么 要一样appKey+ appSecret 这种成对出现的机制呢?
因为要加密,通常在首次验证(类似登录场景), 用appKey(标记要申请的权限有哪些) + appSecret (密码,表示你真的拥有这个权限)来申请一个token, 就是我们经常用到的 accessToken (通常拥有失效时间), 后续的每次请求都需要提供 accessToken 表示验证权限通过。
现在有了统一的appId,此时如果针对同一个业务要划分不同的权限,比如,同一功能,某些场景需要只读权限,某些场景需要读写权限,这样提供了一个appId 和对应的秘钥appSecret 就没有办法满足需求,此时就需要根据权限进行账号分配,通常用 appKey 和 appSecret
可以生成两对appKey 和 appSecret , 一个用于删除,一个用于读写,达到权限的细粒度划分,如 appKey1 + appSecret1 只有删除权限,但是 appKey2 + appSecret2 有读写权限,这样你就可以把对应的权限发放给不同的开发者。其中权限的配置都是跟 appKey 做关联的,appKey 也需要添加数据库索引,方便快速查找。
简化场景:
第一种场景: 通常用于开放性接口,像地图API,会剩余app_id 和app_key ,此时相当于三者相等,合二为一,appId = appKey = appSecret ,这种模式下,带上 appId 的目的仅仅是统计某个拥挤调用接口的次数而已。
第二种场景,当每一个用户有且仅有一套权限配置,可以去掉appKey ,直接将 appId = appKey ,每个用户分配一个 appId + appSecret 就够了。
也可以采用签名(signature) 方式: 当调用三方服务提供的方法发起请求的时候,带上(appKey 、时间戳 timeStamp 、随机数nonce 、签名sigj), 其中 签名sign 可以使用 AppSecret + 时间戳 + 随机数 使用 sha1 、MD5生成, 服务提供方后收到后,生成本地签名和收到的签名比对,如果一致,校验成功。
签名规则
1、分配appId(开发者标识) 和 appSecret (密钥) 给不同的调用方
可以直接通过平台线上申请,也可以直接线下直接颁发,appId 是全局唯一的,每个appId将对应一个客户,密钥appSecret 需要高度保密
2、加入 timeStamp(时间戳),以服务端当时时间为准,单位为ms, 5分钟内数据有效
时间戳的目的就是为了减轻DOS攻击,防止请求被拦截后一直尝试请求接口,服务端设置时间阈值,如果服务器时间 - 请求时间戳 超过阈值,表示签名超时,接口调用失败。
3、加入临时流水号nonce ,至少为10位,有效期内放置重复提交。
随机值nonce 主要是为了增加签名sign多边形,也可以保护接口的幂等性,相邻的两次请求nonce 不允许重复,如果重复则认为是重复提交,接口调用失败。
① 针对查询接口,流水号只用于日志落地,便于后期日志核查。
② 针对办理类接口需要校验流水号在有效期的唯一性,以避免重复请求。
通过在接口签名请求参数加上 时间戳 timeStamp +随机数 可以防止重放攻击。
1、时间戳(timeStamp)
以服务端当前时间为准,服务端要求客户端发过来的时间戳,必须是最近60秒内(假设值,自己定义) 的
这样,即使这个请求即使被截取了,也只能在60s内进行重放攻击。
2、随机数(nonce)
但是,即使设置了时间戳,攻击者还有60s的攻击时间呢!
所以我们需要再客户端请求中再加上一个随机数(中间黑客不可能自己随机修改参数,因为有参数签名校验呢) ,服务端会对一分钟内请求的随机数进行检查,如果有两个相同的,基本可以判定为重放攻击。
因为正常情况下,在短时间内(比如60s)连续生成两个相同nonce 的情况几乎为0。
服务端"第一次"在接收到这个nonce 的时间做下面行为。
1、去redis 中查找是否有key 为nonce 的数据
2、如果没有,则创建这个key, 把这个key 失效时间和验证timestamp 失效时间一致,比如是60s
3、如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。
4、加入签名字段sign,获取调用方传递的签名信息。
通过在接口签名请求参数加上 时间戳appId + sign 解决身份验证和防止 “参数篡改”
1、请求携带参数appId 和Sign ,只有拥有合法的身份appId 和正确的签名sign 才能放行,这样就解决了身份验证和参数篡改问题。
2、即使请求参数被劫持,由于获取不到appSecret (仅作为本地加密使用,不参与网络传输),也无法伪造合法请求。
以上字段放在请求头中。 其中nonce 应该是一个随机的、唯一的字符串,可以使用UUID或者其他随机字符串生成算法来创建。TimeStamp 表示请求的时间戳,通常使用标准的Unix 时间戳格式(以秒为单位)
总结: ① 提供appId(用户账户id) 和 appSecret (加密秘钥,用于参数加密后服务端的二次校验)
② 参数中使用timeStamp(时间戳,保证请求的实时性) 和nonce (保证在有效期内放置被重放).其中nonce 一般有uuid 或者随机字符串生成,服务端接收到请求后,记录到缓存中,其缓存过期时间为有效期长短,在有效期内如果是重复的则认为是重放请求。
服务端安全性
单个接口针对ip 限流
使用redis进行接口调用次数统计,ip + 接口地址作为key,方位次数作为value ,每次请求value + 1 ,设置过期时长来限制接口的调用频率。
记录接口请求日志
记录请求日志,快速定位异常请求位置,排查问题原因,如使用aop 来全局处理接口请求
敏感数据脱敏
在接口调用过程中,可以会涉及到订单号等敏感数据,这类数据通常需要脱敏处理。
最常用的方式就是加密,加密方式使用安全性比较高的RSA 非对称加密。非堆成加密算法有两个秘钥,这两个秘钥完全不同单又完全屁屁,只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。
幂等性问题(重复更新或者新增)
①提供一个生成随机数的接口,随机数全局唯一,调用接口的时候带入随机数。
② 第一次调用,业务处理成功后,将随机数作为key,操作结果作为value,存入redis ,同时设置过期时长。
③第二次调用,查询redis,如果key存在,则证明重复提交,直接返回错误。
响应状态码规范
状态码设计参考如下:
public enum CodeEnum {
SUCCESS(200,"处理成功"),
ERROR_PATH(404,"请求地址错误"),
ERROR_SERVER(505,"服务器内部发生错误");
private int code;
private String message;
CodeEnum(int code, String message) {
this.code = code;
this.message = message ;
}
private void setCode(int code) {
this.code = code;
}
private int getCode() {
return this.code;
}
private void setMessage(String message) {
this.message = message;
}
private String getMessage() {
return message;
}
}
统一响应数据格式
为了方便给客户端响应,响应数据会包含三个属性,状态码(code) 、信息描述(message) 、响应数据(data),客户端根据状态码以及新秀描述可快速知道直接够,如果状态码返回成功,再开始处理数据。
public class Result implements Serializable {
private static final long serialVersionUID = 111L;
private int code;
private String message;
private Object data = null;
private int getCode() {
return code;
}
private void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Result fillData(Object data) {
this.setCode(CodeEnum.SUCCESS.getCode());
this.setMessage(CodeEnum.SUCCESS.getMessage());
this.data = data;
return this;
}
}