公司有个业务用户看一条广告,就往自己钱包里面添加相应的金额。自己通过抓包,可以重放请求或直接构造金额给后端。因为参数没有加密,活在惶恐之中。虽然用的用户不多,但总怕有人抓包搞事情。遂看有没有现成的加密轮子。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)) { 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); String headerRsa = request.getHeader(headerFlag); String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey); String aesPassword = EncryptUtils.decryptByBase64(decryptAes); long secoend = Math.abs(Instant.now().getEpochSecond() - Long.parseLong(aesPassword.substring(0, 10)));
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
|
@ApiEncrypt() @PostMapping("/payAdRevenue") public Result<Boolean> payAdRevenue(@RequestBody PayAdRevenueVO payAdRevenueVO){ return Result.judge(userWalletInformationBiz.payAdRevenue(payAdRevenueVO.getAmount())); }
|
接口测试工具测试
Body内容
Header内容
遗留问题
这个还有个问题,通过加密可以让用户不能直接构造参数来进行充值,但是用户可以通过重放抓到的接口来进行充值。
带上时间戳相关的AES密钥。
把时间戳写在请求体中,接口校验两次请求的时间是否过期(假如设置10秒?)这个度量不好把握,而且在短时间内还是可以充值。即使加上接口限流还是没有效果。
可以在请求连接上带一个随机参数,然后把这个参数放在post请求加密一起发过来。处理一个请求就把这个请求连接放入缓存记录下来。下次来相同的请求直接检查缓存是否已经请求过了。放入post请求在解密时就可以知道这个请求是否合法了。