ruoyi-common-encrypt包的使用.md

公司有个业务用户看一条广告,就往自己钱包里面添加相应的金额。自己通过抓包,可以重放请求或直接构造金额给后端。因为参数没有加密,活在惶恐之中。虽然用的用户不多,但总怕有人抓包搞事情。遂看有没有现成的加密轮子。ruoyi-common-encrypt就是一个现成的。

实现原理

common-encrypt 核心类是filter文件夹下面的CryptoFilter类。通过提前拦截ServletRequest,response,来对进出信息进行加密解密。

从这段代码可以看到,把密钥放到Header中的一个参数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
// 是否为 put 或者 post 请求
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
// 是否存在加密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
responseFlag = apiEncrypt != null && apiEncrypt.response();
if (StringUtils.isNotBlank(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
} else {
// 是否有注解,有就报错,没有放行
if (ObjectUtil.isNotNull(apiEncrypt)) {
HandlerExceptionResolver exceptionResolver = SpringUtil.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
exceptionResolver.resolveException(
servletRequest, servletResponse, null,
new BizException("没有访问权限,请联系管理员授权403-FORBIDDEN"));
return;
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
}
}

看看请求解密的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException{
super(request);
// 获取 AES 密码 采用 RSA 加密
String headerRsa = request.getHeader(headerFlag);
String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
// 解密 AES 密码
String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
long secoend = Math.abs(Instant.now().getEpochSecond() - Long.parseLong(aesPassword.substring(0, 10)));
// if (secoend > 30) {
// throw new BizException("非法请求");
// }
request.setCharacterEncoding("UTF-8");
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
String decryptBody = null;
try {
decryptBody = EncryptUtils.decryptAes(requestBody, aesPassword);
} catch (Exception e) {
throw new RuntimeException(e);
}
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}

这个方法可以看出,是把AES 密码先转为base64.再用RSA公钥加密放入Header的参数中。然后用AES密钥加密请求体发给后台。解密过程就是,读取Header参数用RSA私钥解密得到base64,再得到AES密钥,再去解密请求载荷。思路和HTTPS过程很像。

使用方法

在配置文件中配置这个包的必要参数.

1
2
3
4
5
api-decrypt:
enabled: true
headerFlag: X-Api-Signature
publicKey:
privateKey:

然后在需要加密的接口上直接写注解@ApiEncrypt()

1
2
3
4
5
6
7
8
9
/**
* 添加用户广告收益
* @return
*/
@ApiEncrypt()
@PostMapping("/payAdRevenue")
public Result<Boolean> payAdRevenue(@RequestBody PayAdRevenueVO payAdRevenueVO){
return Result.judge(userWalletInformationBiz.payAdRevenue(payAdRevenueVO.getAmount()));
}

接口测试工具测试

Body内容

image-20240201115544343

Header内容

image-20240201115608708

遗留问题

这个还有个问题,通过加密可以让用户不能直接构造参数来进行充值,但是用户可以通过重放抓到的接口来进行充值。

  1. 带上时间戳相关的AES密钥。

    把时间戳写在请求体中,接口校验两次请求的时间是否过期(假如设置10秒?)这个度量不好把握,而且在短时间内还是可以充值。即使加上接口限流还是没有效果。

  2. 可以在请求连接上带一个随机参数,然后把这个参数放在post请求加密一起发过来。处理一个请求就把这个请求连接放入缓存记录下来。下次来相同的请求直接检查缓存是否已经请求过了。放入post请求在解密时就可以知道这个请求是否合法了。


ruoyi-common-encrypt包的使用.md
https://lililib.github.io/ruoyi-common-encrypt包的使用/
作者
煨酒小童
发布于
2024年2月1日
许可协议