在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。
重放攻击,web漏洞中称会话重放漏洞,又称重播攻击、回放攻击。
指的是先截取主机A发送给主机B的报文,入侵者把A请求B的报文原封不动地再发送一次,两次...n次,使主机B误以为入侵者就是主机A,然后进入到正常逻辑中并返回响应。如果是付款接口,或者购买接口就会造成损失,因此需要采用防重放的机制来做请求验证,如请求参数上加上时间戳(timestamp)和随机数(nonce)。
不可以,加密可以有效防止明文数据被监听,但是却防止不了重放攻击。
用来标识你的开发者账号的,即:用户id,可以在数据库添加索引,方便快速查找,app_key和app_secret是一对出现的账号, 同一个appId可以对应多个appKey + appSecret,这样平台就可以分配你不一样的权限, 比如app_key1 + app_secect1只有只读权限,但是app_key2 + app_secret2有读写权限… 这样你就可以把对应的权限放给不同的开发者;其中权限的配置都是直接跟app_key做关联的,app_key也需要添加数据库检索,方便快速查找。
公开的,调用服务所需要的密钥。是用户的身份认证标识,用于调用平台可用服务,可以简单理解成是账号。
签名的密钥,是跟appKey配套使用的,可以简单理解成是密码。
向第三方服务器请求授权时,带上AppKey和AppSecret(需存在服务器端)。
第三方服务器验证appKey和appSecret在数据库、缓存中有没有记录;如果有,生成一串唯一的字符串(token令牌),返回给服务器,服务器再返回给客户端,后续客户端每次请求都需要带上token令牌。
因为要加密, 通常用在首次验证(类似登录场景), 用appKey(标记要申请的权限有哪些)+appSecret(密码,表示你真的拥有这个权限)来申请一个token, 就是我们经常用到的accessToken(通常拥有失效时间),后续的每次请求都需要提供accessToken表明验证权限通过。
现在有了统一的appId,此时如果针对同一个业务要划分不同的权限,比如同一功能,某些场景需要只读权限,某些场景需要读写权限。这样提供一个appId和对应的秘钥appSecret就没办法满足需求。此时就需要根据权限进行账号分配,通常使用appKey和appSecret。
由于appKey和appSecret是成对出现的账号,同一个appId可以对应多个 appKey + appSecret,这样平台就为不同的appKey + appSecret对分配不一样的权限。
第一种场景:通常用于开放性接口,像地图api,会省去app_id和app_key,此时相当于三者相等,合而为一appId = appKey = appSecret。这种模式下,带上app_id的目的仅仅是统计某一个用户调用接口的次数而已。
第二种场景:当每一个用户有且仅有一套权限配置可以去掉appKey,直接将app_id = app_key,每个用户分配一个appId + appSecret就够了。
也可以采用签名(signature)的方式:当调用方向服务提供方法发起请求时,带上(appKey、时间戳timeStamp、随机数nonce、签名sign)签名sign可以使用 (appSecret + 时间戳 + 随机数)使用sha1、md5生成,服务提供方收到后,生成本地签名和收到的签名比对,如果一致,校验成功。
分配appId(开发者标识)和appSecret(密钥),给不同的调用方。
可以直接通过平台线上申请,也可以线下直接颁发。appId是全局唯一的,每个appId将对应一个客户,密钥appSecret需要高度保密。
加入timeStamp(时间戳),以服务端当前时间为准,单位为ms,5分钟内数据有效。间戳的目的就是为了减轻DOS攻击。防止请求被拦截后一直尝试请求接口。服务器端设置时间戳阀值,如果服务器时间 减 请求时间戳超过阀值,表示签名超时,接口调用失败。
加入临时流水号nonce,至少为10位,有效期内防重复提交。
随机值nonce主要是为了增加签名sign的多变性,也可以保护接口的幂等性,相邻的两次请求nonce不允许重复,如果重复则认为是重复提交,接口调用失败。
针对查询接口,流水号只用于日志落地,便于后期日志核查。
针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
通过在接口签名请求参数加上时间戳(timeStamp)+ 随机数(nonce)可以防止 “重放攻击”。
以服务端当前时间为准,服务端要求客户端发过来的时间戳,必须是最近60秒内(假设值,自己定义)的。这样,即使这个请求即使被截取了,也只能在60s内进行重放攻击。
但是,即使设置了时间戳,攻击者还有60s的攻击时间呢!
所以我们需要在客户端请求中再加上一个随机数(中间黑客不可能自己修改随机数,因为有参数签名的校验呢),服务端会对一分钟内请求的随机数进行检查,如果有两个相同的,基本可以判定为重放攻击。
因为正常情况下,在短时间内(比如60s)连续生成两个相同nonce的情况几乎为0。
服务端“第一次”在接收到这个nonce的时候做下面行为:
1)去redis中查找是否有key为nonce:{ nonce}的数据
2)如果没有,则创建这个key,把这个key失效的时间和验证timestamp失效的时间一致,比如是60s。
3)如果有,说明这个key在60s内已经被使用了,那么这个请求就可以判断为重放请求。
加入签名字段sign,获取调用方传递的签名信息。
通过在接口签名请求参数加上appId + sign解决身份验证和防止“参数篡改”。
1)请求携带参数appId和Sign,只有拥有合法的身份appId和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题。
2)即使请求参数被劫持,由于获取不到appSecret(仅作本地加密使用,不参与网络传输),也无法伪造合法的请求。
鉴权参数 = 请求头签名参数(appId,timeStamp,nonce) + 请求URL地址(调用方请求接口完整url地址) + 请求Request参数(针对是Get请求时) + 请求Body(非针对是Get请求时 ,如Post请求)。
先将鉴权参数以key-value的格式存储,并以key值正序排序,进行拼接。如: key1value1key2value2;最后将上面拼接的字符串在拼接应用密钥appSecret,如: key1value1key2value2 + appSecret;将最终拼接成的字符串转成utf-8的字节数组,后然后做Md5不可逆加密 Md5(key1value1key2value2 + appSecret) 得到的字符串作为签名signature。
请求头="appId=xxxx&nonce=xxxx&tamp=xxxx&sign=xxx"
请求头中的4个参数是必须要传的,否则直接报异常。
请求该接口的完整地址。
请求数据的拼接规则:
Path:按照path中的顺序将所有value进行拼接。
URL 路径参数指的是通过在 URL 的斜杠后面传递的参数。比如我们要访问id为2的project, 则可以访问/project/2这个URL。
Query:按照key的字典顺序排序,将所有key=value进行拼接。
查询字符串参数(query string)和 路径参数类似,你也可以通过查询字符串的形式传递id。查询字符串就是在url中通过?号后面加参数。比如/project/?id=2这种形式。
Form:按照key的字典顺序排序,将所有key=value进行拼接。
表示为表单请求时携带的数据。
Body:
表示是一个raw数据请求(纯字符串格式),比如json的方式传递。
Json: 按照key的字典顺序排序,将所有key=value进行拼接
(例如{“a”:“a”,“c”:“c”,“b”:{“e”:“e”}} => a=a_b=e=e_c=c)
String: 整个字符串作为一个拼接。
分别对应 SpringMvc提供的获取参数的注解:
@RequestParam:处理(前端)Content-Type为 application/x-www-form-urlencoded或者form-data编码的内容
@PathVariable:模板变量,一般用于get请求,即 XXX/{XXXid}
@RequestBody:常用来处理Content-Type为application/json, application/xml码的内容,前端规定的是raw方式。
如果存在多种数据形式,同种数据内按照上面描述的四种规则进行拼接,拼接好后,不同的数据格式则按照path、query、form、body的顺序进行二次拼接,得到所有数据最终的拼接值。
基本原理其实也比较简单,就是自定义过滤器或者拦截器,对每个请求进行拦截处理,在服务端取到调用方的参数后按同样的签名规则进行匹配。
1)验证请求头参与签名的必传参数;
2)获取请求头参数,Url请求路径 ,请求数据,把这些值放入SortMap中进行排序;
3)对SortMap里面的值进行拼接;
4)对拼接的值进行加密,生成签名sign;
5)把后台生成的签名sign和调用方传入的签名sign进行比较,如果不相同就返回错误;
一般调用接口最常用的两种方式就是GET和POST。两者的区别也很明显,GET请求会将参数暴露在浏览器URL中,而且对长度也有限制。为了更高的安全性,所有接口都采用POST方式请求。
ip白名单是指将接口的访问权限对部分ip进行开放来避免其他ip进行访问攻击。
限流是为了更好的维护系统稳定性。
使用redis进行接口调用次数统计,ip+接口地址作为key,访问次数作为value,每次请求value+1,设置过期时长来限制接口的调用频率。
记录请求日志,快速定位异常请求位置,排查问题原因。(如:用aop来全局处理接口请求)
在接口调用过程中,可能会涉及到订单号等敏感数据,这类数据通常需要脱敏处理。
最常用的方式就是加密。加密方式使用安全性比较高的RSA非对称加密。 非对称加密算法有两个密钥,这两个密钥完全不同但又完全匹配。只有使用匹配的一对公钥和私钥,才能完成对明文的加密和解密过程。
幂等性是指: 任意多次请求的执行结果和一次请求的执行结果所产生的影响相同。
提供一个生成随机数的接口,随机数全局唯一。调用接口的时候带入随机数。
第一次调用,业务处理成功后,将随机数作为key,操作结果作为value,存入redis,同时设置过期时长。
第二次调用,查询redis,如果key存在,则证明是重复提交,直接返回错误。
一套成熟的API文档,一旦发布是不允许随意修改接口的。这时候如果想新增或者修改接口,就需要加入版本控制,版本号可以是整数类型,也可以是浮点数类型。
一般接口地址都会带上版本号,http://ip:port//v1/list , http://ip:port//v2/list
一套成熟的API,还需要提供简单明了的响应值,根据状态码就可以大概知道问题所在。我们采用http的状态码进行数据封装,例如200表示请求成功,4xx表示客户端错误,5xx表示服务器内部发生错误。
为了方便给客户端响应,响应数据会包含三个属性,状态码(code),信息描述(message),响应数据(data)。客户端根据状态码及信息描述可快速知道接口,如果状态码返回成功,再开始处理数据。
一套成熟的API,还少不了一个优秀的接口文档。接口文档的可读性非常重要,虽然很多程序员都不喜欢写文档,而且不喜欢别人不写文档。为了不增加程序员的压力,推荐使用swagger2或其他接口管理工具,通过简单配置,就可以在开发中测试接口的连通性,上线后也可以生成离线文档用于管理API。
第1步: 将所有参数(注意是所有参数,包括appId,timeStamp,nonce),除去sign本身,以及值是空的参数,按key名升序排序存储。
第2步: 然后把排序后的参数按 key1value1key2value2…keyXvalueX的方式拼接成一个字符串。(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将"转成”后再拼接)。
第3步: 把分配给调用方的密钥secret拼接在第2步得到的字符串最后面。
即: key1value1key2value2…keyXvalueX + secret
即: Md5(key1value1key2value2…keyXvalueX + secret) 转大写。
API签名验证工具: API签名验证工具,接口数据的安全问题。