安全优雅的RESTful API签名实现方案
1、接口签名的必要性
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题。其中我认为最终要的还是数据是否被篡改。在此分享一下我的关于接口签名的实践方案。
2、项目中签名方案痛点
- 每个接口有各自的签名方案,不统一,维护成本较高。
- 没有对消息实体进行签名,无法避免数据被篡改。
- 无法避免数据重复提交。
3、签名及验证流程
4、签名规则
- 线下分配appid和appsecret,针对不同的调用方分配不同的appid和appsecret。
- 加入timestamp(时间戳),10分钟内数据有效。
- 加入流水号nonce(防止重复提交),至少为10位。针对查询接口,流水号只用于日志落地,便于后期日志核查。 针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
- 加入signature,所有数据的签名信息。
其中appid、timestamp、nonce、signature这四个字段放入请求头中。
5、签名生成
5.1、数据部分
- Path:按照path中的顺序将所有value进行拼接
- Query:按照key字典序排序,将所有key=value进行拼接
- Form:按照key字典序排序,将所有key=value进行拼接
- Body:
如果存在多种数据形式,则按照path、query、form、body的顺序进行再拼接,得到所有数据的拼接值。
上述拼接的值记作 Y。
5.2、请求头部分
X="appid=xxxnonce=xxxtimestamp=xxx"
5.3、生成签名
最终拼接值=XY
最后将最终拼接值按照如下方法进行加密得到签名(signature)。
6、签名算法实现
6.1、指定哪些接口或者哪些实体需要签名
6.2、指定哪些字段需要签名
6.3、签名核心算法(SignatureUtils)
- toSplice方法首先判断对象是否注有@Signature注解,如果有则获取签名的排序规则(key值字典序排序或者指定order的值进行排序),比如排序规则是Signature.ALPHA_SORT(字典序)会调用alphaSignature方法生成key=value的拼接串;如果对象没有@Signature注解,该对象类型可能是数组、者集合类等,则调用toString方法生成key=value的拼接串。
- alphaSignature方法通过反射获取到对象的所有Field属性,需要判断两种情况:(1)获取该Field属性对应的Class信息,如果Class信息含有@Signature注解,则调用toSplice方法生成key=value的拼接串;(2)该Field属性含有@SignatureField注解,调用toString方法生成key=value的拼接串。
- toString方法针对array, collection, simple property, map类型的数据做处理。其中如果对象是java的simple property类型,直接调用对象的toString方法返回value;如果是array、collection、map类型的数据,再调用toSplice方法生成key=value的拼接串。
7、签名校验
7.1、header中参数
7.2、签名实体SignatureHeaders, 绑定request中header信息
7.3、生成签名实体SignatureHeaders并验证
签名验证需要如下几个步骤。
- 处理header name,通过工具类将header信息绑定到签名实体SignatureHeaders对象上。
- 验证appid是否合法。
- 根据appid从配置中心中拿到appsecret。
- 请求是否已经超时,默认10分钟。
- 随机串是否合法。
- 是否允许重复请求。
- 重新生成签名,验证签名是否一致。
7.4、生成header信息参数拼接
7.5、切面拦截控制层方法,生成method中参数的拼接
generateAllSplice方法是在控制层切面内执行,可以在方法执行之前获取到已经绑定好的入参。分别对注有@PathVariable、@RequestParam、@RequestBody、@ModelAttribute注解的参数进行参数拼接的处理。其中注@RequestParam注解的参数需要特殊处理一下,分别考虑数组、集合、原始类型这三种情况。
7.6、对最终的拼接结果重新生成签名信息
8、客户端使用示例
8.1、生成签名
8.2、输出结果
拼接结果: appid=111^_^appsecret=222^_^nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67^_^timestamp=1538207443910^_^w8rAwcXDxcDKwsM=^_^
签名数据: SignatureHeaders{appid=111, appsecret=222, timestamp=1538207443910, nonce=c9e778ba668c8f6fedf35634eb493af6304d54392d990262d9e7c1960b475b67, signature=0a7d0b5e802eb5e52ac0cfcd6311b0faba6e2503a9a8d1e2364b38617877574d}
请求数据: [w8rAwcXDxcDKwsM=]
9、思考
上述的签名方案的实现校验逻辑是在控制层的切面内完成的。如果项目用的是springmvc框架,可以放在Filter或者拦截器里吗?很明显是不行的(因为ServletRequest的输入流InputStream 在默认情况只能读取一次)。上述方案需要获取绑定后的参数结果,然后执行签名校验逻辑。在执行控制层方法之前,springmvc已经帮我们完成了绑定的步骤,当然了,在绑定的过程中会解析ServletRequest中参数信息(例如path参数、parameter参数、body参数)。
其实如果我们能在Filter或者拦截器中实现上述方案,那么复杂度将会大大的降低。首先考虑如何让ServletRequest的输入流InputStream可以多次读取,然后通过ServletRequest获取path variable(对应@PathVariable)、parameters(对应@RequestParam)、body(对应@RequestBody)参数,最后整体按照规则进行拼接并生成签名。
优化方案参考:https://www.cnblogs.com/hujunzheng/p/10178584.html