目的

         为了加强客户端请求服务器的安全性, 客户端对请求进行签名,服务器对请求校验,防止恶意请求。

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,
    "id":17260269,
    "addressDesc":{
        "receiver":"中文",
         "phone":"13333333333",
        "address":{
            "city":"abc",
            "detail":"算哒算哒"
        }
    },
    "timestamp":1668741030000,
    "nonce":"0ccb9817e9c6-441f0-be77-d002fc2caaab"
}
在有多层json对象嵌套时,内层的对象需要前端和后端约定传参顺序。
前端可通过有序map存储(如linkedHashMap,treeMap等),
后端需要指定对象的转换顺序

 

后端转换保持字段顺序不变:

 

转换后:activityId=1&addressDesc={"receiver":"中文","phone":"13333333333","address":{"city":"abc","detail":"算哒算哒"}}&nonce=0ccb9817e9c6-441f0-be77-d002fc2caaab&timestamp=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"
                }
        }
    ],
    "timestamp":1668750396000,
    "nonce":"0ccb9817e9c6-4222"
}

                                    转换后:

                           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&timestamp=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)前端添加验签失败的提示。

 

posted on 2023-07-10 10:58  毛会懂  阅读(74)  评论(0编辑  收藏  举报