目的
为了加强客户端请求服务器的安全性, 客户端对请求进行签名,服务器对请求校验,防止恶意请求。
2、大运营
大运营游戏级别添加控制是否验签的开关, 当打开时,活动接口才会验签(只针对后续支持验签的游戏)。
当验签开关打开时,需要输入secret,保存在服务端。游戏开发时,也要保存在游戏前端。(每个游戏一个秘钥,确保唯一性,不作为接口的参数传参,注意:确保不易被穷举,生成算法不易被猜测,游戏上线后不再更改)。
接口必须传activityId,服务端根据activityId查到gameId,再根据gameId查secret。
服务端验签的接口(以下二选一):
(1)如果存在接口中不传activityid的情况,则不在验签。
(2)接口加验签注解。
secret 生成:
public static void main(String[] args) {
String str="you@xi%pingTAI&activityId=7" + new Random().nextInt(100);
String md5Str = DigestUtils.md5Hex(str);
log.info("MD5的值为:{}",md5Str);
}
模块:
moduleName=address
9a90ef41cf14c7cb1c5c313d35492c05(地址模块 已适用)
moduleName=couponcode
54bacd2b58ea530a4808f044468c576a(券码模块 已使用)
3、客户端MD5签名
3.1需要参与签名的参数
在请求参数列表中,除无值的字段 ,值为null的字段,branchId,tenantId,currentPage,pageSize,showWinningRecord,winningStrategy,needAsc,reusable,subscribe,isSession,isShowPoint,score
(理论上有值的字段必须验签,但后端接口字段有默认值导致验签失败。重要的字段必须参与验签)
之外,其他需要使用到的参数都是需要签名的参数。
把签名后的值存储在请求headers的 sign字段中。
timestamp和nonce也要传参和签名。
(1)加随机数(不采用)
该方法优点是认证双方不需要时间同步,双方记住使用过的随机数,如发现请求中有以前使用过的随机数,就认为是重放攻击。缺点是需要额外保存使用过的随机数,若记录的时间段较长,则保存和查询的开销较大。
(2)加时间戳(不采用)
该方法优点是不用额外保存其他信息。缺点是认证双方需要准确的时间同步,同步越好,受攻击的可能性就越小。但当系统很庞大,跨越的区域较广时,要做到精确的时间同步并不是很容易。
客户端第一次访问时,将签名sign存放到服务器的Redis中,超时时间设定为跟时间戳的超时时间一致,
二者时间一致可以保证无论在timestamp限定时间内还是外 URL都只能访问一次,如果被非法者截获,使用同一个URL再次访问,
如果发现缓存服务器中已经存在了本次签名,则拒绝服务(其实这样是有问题的,比如获取活动详情接口,如果两个人在同一个时间戳获取活动详情,其中一个请求会被拒绝)。
如果在缓存中的签名失效的情况下,有人使用同一个URL再次访问,则会被时间戳超时机制拦截,这就是为什么要求sign的超时时间要设定为跟时间戳的超时时间一致。
拒绝重复调用机制确保URL被别人截获了也无法使用(如抓取数据)。
(3)timestamp+nonce(采用)
nonce取值范围:8位整数的随机数 或 直接使用时间戳的16进制 或 客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。
nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数“集合”越来越大的问题。防止重放攻击一般和防止请求参数被串改一起做.
接口请求中添加nonce字段,nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在1分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在1分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,
如存在已有集合,则拒绝。否则,记录该nonce。
删除集合内时间戳大于1分钟的nonce(可以使用redis的expire,新增nonce的同时设置它的超时失效时间为1分钟)。
3.2 生成sign值
3.2.1 对于如下的请求参数:
http://xxxx.xxx.xxx.com/rest?order_id=2985082433&tp_customer_phone=13000000001&reason_code=1
把所有请求参数的名称根据参数名进行升序排列,加上参数值后,每个参数用“&”字符连接起来,如:
order_id=2985082433&reason_code=1&tp_customer_phone=13000000001
这拼接后的字符串便是请求参数排序字符串。
注意:(1)如果传了无值 或 null 的参数,那么不应该拼接到验签的字符串中。
(2)字段的值统一转换成字符串类型。
(3)根据HTTP 协议要求,传递参数的值中如果存在特殊字符(如:&、@等),那么该值需要做URL Encoding,这样请求接收方才能接收到正确的参数值。
这种情况下,待签名数据应该是原生值而不是encoding 之后的值。例如:调用某接口需要对请求参数email 进行数字签名,那么待签名数据应该是email=test@dhf100.com,而不是email=test%40dhf100.com。
例如1:
请求入参:
转换后:activityId=1&addressDesc={"receiver":"中文","phone":"13333333333","address":{"city":"abc","detail":"算哒算哒"}}&nonce=0ccb9817e9c6-441f0-be77-d002fc2caaab×tamp=1668748347000
例如2:对List类型的转换:
转换前:
转换后:
activityId=1&id=17260269&list=[{"receiver":"中文1","phone":"11111","address":{"city":"abc111","detail":"算哒算哒111"}},{"receiver":"中文2","phone":"2222","address":{"city":"abc222","detail":"算哒算哒222"}}]&nonce=0ccb9817e9c6-4222×tamp=1668750396000
3.2.2 secret不参与接口传输,但参与签名。
将secret以&的符号加到请求字符串的前面。
secret=SecretStr&order_id=2985082433&reason_code=1&tp_customer_phone=13000000001
这个字符串就是最终的待签名的字符串。
3.2.3 执行Base64.encode(MD5(待签名的字符串)),生成sig值:
如:ZTA1ODNjY2YxMTlkN2YyNjQzMzBiZDNkZmRiMWUyNjU=。 (这个值不对,只供参考)
3.2.4 把签名后的值存储在请求headers的 sign字段中,传输到后端。
4、服务器端验证请求的正确性
(0)请求参数转换:
(1)将请求的参数(除去的字段:sign字段,字段值转换为字符串后为空的字段)排序。如order_id=2985082433&reason_code=1&tp_customer_phone=13000000001
(2)后续步骤参考 3.2节
(3) 后端从headers的sign字段取值。
(4)校验结果 :将上一步生成的sig值和请求参数的 sig值比较, 如果一致则说明请求正确。 继续后续操作,否则返回错误码。
5、测试重点
只对post请求并且参数中有活动id的验签
(1)参数中无值 或 null的字段不参与签名。
(2) page 和 size 后端有默认值。最终解决方案:不参与签名
(3) 服务端接口参数有设置默认值的情况(比如发奖类型默认概率奖,是否会影响验签),默认值必传(后端无法区分是服务端的默认值还是前端传的值)。 最终解决方案:不参与签名
(4) 服务端自己用的默认的字段(比如有些Boolean 类型服务端自己默认了)。最终解决方案:不参与签名
(5)参数中含有特殊字符(如邮箱中的@,字符串中的&) 按原始字符签名。
(6)参数中有空数组( [])或 空对象({})的 需要参与签名。
(7) post请求参数有多个的情况
@PostMapping("/miya/coupon/wxNotify")
public ApiResult miYaCouponWxNotify(@RequestBody MiYaDTO miYaDTO, HttpServletRequest request)
(8)跳转是get请求的情况。
@GetMapping("/{id}")
public void getUrl(HttpServletResponse resp, @PathVariable("id") Integer id, HttpServletRequest request) throws IOException
(9) 接口参数为List类型。
@PostMapping("/whiteExport/list")
public ApiResult whiteExport(@RequestBody List<ParticipateWhitelistDTO> whiteList)
(10) 文件上传接口(UtilController类)
@PostMapping("/uploadFile")
@ResponseBody
public ApiResult upload(MultipartFile file)
(11)webSocket不验签
6、其他
(1)服务端接口访问限制频率
通过sentinel限制。
如果是对IP限制频率,则需要redis限制。
(2)服务端添加IP黑名单。 对恶意的IP加入黑名单。
(3)服务端添加IP白名单。 比如 对屏端的游戏,把IP加入白名单,只允许固定的屏能访问。
(4)前端添加验签失败的提示。