一次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;
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的封装肯定是大同小异,毕竟都是从一个地方复制来的
这里主要讲如何修改:
- 找到封装的Request.js
- 找到参数封装处
- 修改代码
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 {
@Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) { return methodParameter.hasMethodAnnotation(Decrypt.class)||methodParameter.hasParameterAnnotation(Decrypt.class); }
@Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { try { byte[] body = new byte[inputMessage.getBody().available()]; inputMessage.getBody().read(body, 0, body.length - 1); String param = new String(body).trim(); 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));
return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return bais; }
@Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } catch (Exception e) { e.printStackTrace(); }
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" }
|
没了