Blank
文章7
标签8
分类5
一次AOP的实践:前后端分离的API参数加密

一次AOP的实践:前后端分离的API参数加密

好久不见了,最近确实没啥好写的,Aciviti也不整了,应该是烂尾了,这次来个写个短篇博客

最近工作接到了一个需求:将Vue做的网页请求的所有接口的参数使用SM4加密
花了九牛二虎之力才有了一个不错的解决方案

项目环境

前端:Vue + ElementUI(UI框架其实不重要反正这波也没用到)
后端:SpringBoot

构思

既然是功能的搭建,必然是想要开发的程序员无感觉开发
最大程度的减少程序员额外的代码量(接口也tm是我写我就想简单一点)
所以,前端request肯定要采用过滤器,处理前台参数。后端接收也要用AOP处理参数

考虑到SM4是对称加密,所以Key是要灵活的

实现

SM4实现

对称加密,没什么难度,直接上代码
Vue版本:
Sm4Utils.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const SM4 = require("gm-crypt").sm4;

export function encrypt(text, key) {
var sm4Config = {
key,
mode: "ecb"
}
var sm4 = new SM4(sm4Config);
return sm4.encrypt(text);
}

export function decrypt(text, key) {
var sm4Config = {
key,
mode: "ecb"
}
var sm4 = new SM4(sm4Config);
return sm4.decrypt(text);
}

这里要注意,要先install下gm-crypt:从名字上来看,g(uo)m(i)-crypt(加密),很好理解
SpringBoot版本:
首先引用依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.60</version>
</dependency>

版本什么就无所谓了,反正跟咱们也没关系,好使的功能还好使
Sm4Utils工具类:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
public class Sm4Utils {

private static String SM4_KEY;

@Value("${sm4.key}")
public void setSm4Key(String sm4Key) {
SM4_KEY = sm4Key;
sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", SM4_KEY.getBytes());
}

//指明加密算法和秘钥
private static SymmetricCrypto sm4;

//加密为16进制,也可以加密成base64/字节数组
public static String encrypt(String plaintext) {
return sm4.encryptHex(plaintext);
}

public static byte[] encrypt(byte[] text){
return sm4.encrypt(text);
}

//解密
public static String decrypt(String ciphertext) {
return sm4.decryptStr(ciphertext);
}

public static byte[] decrypt(byte[] ciphertext) {
return sm4.decrypt(ciphertext);
}

public static String encryptByKey(String plaintext, String key) {
SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", key.getBytes());
return sm4.encryptHex(plaintext);
}

public static byte[] encryptByKey(byte[] text, String key){
SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", key.getBytes());
return sm4.encrypt(text);
}

public static String decryptByKey(String ciphertext, String key) {
SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", key.getBytes());
return sm4.decryptStr(ciphertext);
}

public static byte[] decryptByKey(byte[] ciphertext, String key) {
SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding", key.getBytes());
return sm4.decrypt(ciphertext);
}

}

提供了两个版本:灵活Key与固定Key。如果是当前加密参数的需求,使用灵活Key即可
提供了两个数据格式重载:byte数组与字符串

相信大家都看得懂,不做赘述

前端Request封装

你们对Request的封装肯定是大同小异,毕竟都是从一个地方复制来的
这里主要讲如何修改:

  1. 找到封装的Request.js
  2. 找到参数封装处
  3. 修改代码
1
2
3
4
5
6
7
8
9
//拼接请求JSON,包含:加密字符串、随机字符串、时间戳
var handledData = {
randomStr: createdCode(3),
timeStamp: new Date().getTime()
}

//假如处理前的参数是data
//这里的秘钥是由随机字符串和时间戳组合到一起,要注意随机字符串一定要是三位
handledData.data = encrypt(JSON.stringify(data), handledData.randomStr + handledData.timeStamp);

然后把handledData变量扔到请求里即可

这样做的好处

前端仍然只需要拼接明文参数,无需思考加密问题
这样处理之后,如果处理前参数是:

1
2
3
{
"userId": "blank@kb0103.cn"
}

经过处理,参数就会变成:

1
2
3
4
5
{
"randomStr": "Gmh",
"timeStamp": 1660202385755,
"data": "t+W0HkGokyGv/ij5V3I5389y4i/qjSibuk3jgb8P6ks="
}

有兴趣的同学可以尝试解密一下

后端SpringBoot AOP

首选我们需要区分什么接口需要解密参数,不然报错了可就不好玩了
标签类Decrypt.java:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {
}

当然里面不需要添加任何代码,只是为了区分解密接口

请求AOP类DecryptRequest.java:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {

/**
* 配置支持条件
* 这里是只有方法或者参数有Decrypt注解的时候才生效
*/
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return methodParameter.hasMethodAnnotation(Decrypt.class)||methodParameter.hasParameterAnnotation(Decrypt.class);
}


/**
* 使用aop,在读取请求参数的之前,对请求参数进行解密
*/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try {
//读取请求参数成为字节body
byte[] body = new byte[inputMessage.getBody().available()];
inputMessage.getBody().read(body, 0, body.length - 1);
String param = new String(body).trim();
//获取解密的Key
JSONObject jsonParam = JSON.parseObject(param);
String key = jsonParam.getString("randomStr") + jsonParam.getString("timeStamp");
//解密
String decryptString = Sm4Utils.decryptByKey(jsonParam.getString("data"), key);
//将解密的字节放入字节数组输入流中
ByteArrayInputStream bais = new ByteArrayInputStream(decryptString.getBytes(StandardCharsets.UTF_8));

//返回HttpInputMessage
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return bais;
}

@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
e.printStackTrace();
}

//如果不需要进行处理的话,则直接调用父类的beforeBodyRead,相当于直接返回inputMessage,不做处理
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}

重头戏,AOP实现
前端的随机字符串和时间戳用来解密data值,将date值直接返回,便可以达到解密的效果
接口的改动:

1
2
3
4
5
6
@PostMapping("/testPostMap")
@Decrypt
public String test2(@RequestBody Map<String, Object> paramMap){
System.out.println("paramMap = " + JSON.toJSONString(paramMap));
return "ok";
}

看到了吗,多出来的标签,这个就表示参数需要经过解密

这样做的好处

后端开发人员无需在意加密问题,前端传了什么,老子后端就接了什么,无缝对接

如果后端接收到处理前的参数是

1
2
3
4
5
{
"randomStr": "Gmh",
"timeStamp": 1660202385755,
"data": "t+W0HkGokyGv/ij5V3I5389y4i/qjSibuk3jgb8P6ks="
}

经过处理,参数就会变成:

1
2
3
{
"userId": "blank@kb0103.cn"
}

没了

本文作者:Blank
本文链接:https://www.kb0103.cn/2022/08/11/316994489/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可