Featured image of post SpringBoot接口签名实践

SpringBoot接口签名实践

SpringBoot接口签名实践

参考资料:SpringBoot接口安全设计:接口限流、防重放攻击与签名验证实战(附源码)

1.没有任何安全限制的转账Demo

下面是一个简单的转账demo,用于一个用户向另一个用户转账

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    @RequestMapping("/account/transfer")
    public void transfer(@RequestBody TransferRequest request)  {
        Long from = request.getFrom();
        Long to = request.getTo();
        BigDecimal money = request.getMoney();
        userService.transfer(from, to, money);
        log.info("账户{}向账户{}转账{}元", from, to, money);
    }

    public static class TransferRequest {
        private Long from;
        private Long to;
        private BigDecimal money;
    }

比如张三给李四转账请求如下,发送一个Post请求包,传输内容为Json格式

1
2
3
4
5
6
7
POST /api/account/transfer Http/1.1
Host:127.0.0.1:8080
Content-Type:application/json

{"from":"张三",
  "to":"李四",
  "money":100}

2.请求伪造

由于上面的接口没有任何安全限制,攻击者王五可以通过抓包,修改请求数据包,实现张三转账给李四的钱转给王五

1
2
3
4
5
6
7
POST /api/account/transfer Http/1.1
Host:127.0.0.1:8080
Content-Type:application/json

{"from":"张三",
  "to":"王五",
  "money":100}

3.如何防止请求伪造—签名sign

3.1.实现签名

双方引入密钥—secretkey,接口调用方和接口提供方需要一个相同的密钥,这个密钥不能被第三方知道,密钥就是一串普通的字符串

1
secretkey = b00000-10299291-1002938-28828

接口调用方:利用密钥进行签名sign

可以通过一些算法对请求进行签名如通过密钥和请求体生成签名

1
sign = md5(secretkey+http请求体)

接口调用方:携带签名发送请求

1
2
3
4
5
6
7
8
POST /api/account/transfer Http/1.1
Host:127.0.0.1:8080
Content-Type:application/json
X-Sign:签名

{"from":"张三",
  "to":"王五",
  "money":100}

3.2.服务端:校验签名

服务端接收到请求后对签名进行校验,服务端知道密钥,拿到请求体,计算出签名与请求头中签名进行校验,若不一致表面请求被篡改了,拒绝请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//获取请求体内容
String body = http请求体
//签名
String sign = request.getHeader("X-Sign")
//计算签名
String expectSign = md5(secretkey+body)
//校验签名
if(!expectSign.equals(sign)){
  //签名有误,非法请求
}

4.新的问题—请求重放—并发

张三给李四转账100元,李四将上面数据拦截,一直重放该请求,本来转账100元由于重放问题转账了1000元

1
2
3
4
5
6
7
POST /api/account/transfer Http/1.1
Host:127.0.0.1:8080
Content-Type:application/json

{"from":"张三",
  "to":"李四",
  "money":100}

5.如何防止请求重放呢?(随机字符串nonce+时间戳)

5.1.前端引入随机字符串nonce

可以在请求头中添加一个随机字符串,每次发送请求,该值都改变

1
2
3
4
5
6
7
8
9
POST /api/account/transfer Http/1.1
Host:127.0.0.1:8080
Content-Type:application/json
X-Sign:签名
X-Nonce:随机字符串

{"from":"张三",
  "to":"王五",
  "money":100}

签名的算法也需要改变,将nonce也加入签名算法中

1
sign = md5(secretkey+http请求体+nonce)

5.2.后端处理请求

后端需要确保随机字符串只能被处理一次,请求过来后需要先看Redis中是否存在这个nonce,如果存在,则返回nonce无效,否则后端将nonce保存到redis中,有效期60分钟

1
2
3
4
5
6
7
8
@Autowired
private RedisTemplate<String, String> redisTemplate;

String nonceKey = "SignatureVerificationFilter:nonce:" + nonce;
if(!this.redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 60, TimeUnit.MINUTES)){
    this.write(response, "nonce无效");
    return false;
}

新的问题,由于有效期只有20分钟,20分钟后,请求还是可以重放

上面代码中,由于nonce存放在redis中,有效期是20分钟,这样只能确保一个请求在20分钟内无法重放,但20分钟后,redis中的数据已经过期,同一个请求到达后端,后端会认为这个nonce没有被使用过导致请求重放

5.3.完整版解决方案—引入时间戳(timestamp)

前端发送请求时,将时间戳加入到请求头中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
POST /api/account/transfer Http/1.1
Host127.0.0.1:8080
Content-Typeapplication/json
X-Sign签名
X-Nonce随机字符串
X-TimeStamp当前时间

{"from":"张三",
  "to":"王五",
  "money":100}

签名算法也需要进行修改

1
sign = md5(secretkey+http请求体+nonce+timestamp)

后端对请求进行限制,请求10分钟内有效

1
2
3
4
5
Long timestamp = Long.parseLong(前端请求头中的时间戳);
Long currentTimestamp = System.currentTimeMillis() / 1000;
if (Math.abs(currentTimestamp - timestamp) > 600) {
    // 请求已过期
}

此时已杜绝请求重放

10分钟内请求重放:由于nonce有效期为20分钟,请求到达后端后,发现nonce已经被使用,请求无效

20分钟后请求重放:由于timestamp有效期是10分钟,请求到达后端后,发现时间戳过期,请求无效

https://github.com/moocstudent/sign-sample

By Lsec
最后更新于 Sep 07, 2025 14:52 +0800
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计
¹鵵ҳ