前言

在 App 开发中,为了防止开发者轻易通过抓包获取到接口请求数据和响应数据,我们会对请求参数进行加密,后端通过解密获取,并加密返回给客户端,客户端通过解密获取。

而如果简单的在每个接口的 Controller 中来对请求参数解密,未免有些太傻,且如果想实现只有正式环境需对请求参数加密,测试环境无需加密,如果这种逻辑在 Controller 中写,更过于傻。

这次通过定义注解的方式来实现接口请求参数的统一解密,并在注解中判断是否需要解密。

加解密本身并不是什么有难度的事情,问题是在何时去处理?定义一个过滤器,将请求和响应分别拦截下来进行处理也是一个办法,这种方式虽然粗暴,但是灵活,因为可以拿到一手的请求参数和响应数据。不过 SpringBoot 中给我们提供了 ResponseBodyAdviceRequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便。

所以这篇文章关于接口参数解密我们使用 RequestBodyAdvice 来实现。

定义注解

接下来我们先定义一个注解

1
2
3
4
5
6
7
8
9
10
11
package com.sktk.keepAccount.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

这个注解就是一个标记,在以后使用的过程中,哪个接口/参数添加了 @Decrypt 注解就对哪个接口/参数进行解密。这个定义也比较简单,没啥好说的。

另外还有一点需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON 交互的时候,这两个才有用。不过一般来说接口加解密的场景也都是前后端分离的时候才可能有的事。

实现注解

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.sktk.keepAccount.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sktk.keepAccount.annotation.Decrypt;
import com.sktk.keepAccount.common.core.exception.BaseException;
import com.sktk.keepAccount.common.core.exception.SystemErrorType;
import com.sktk.keepAccount.common.core.util.AESUtil;
import com.sktk.keepAccount.common.core.vo.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 请求参数解密
* http://www.zzvips.com/article/187109.html
*/
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {

@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}

@Override
public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {

if (targetType == null) {
throw new BaseException(SystemErrorType.BUSINESS_ERROR, "sdf");
}

try {

// 获取请求数据byte
byte[] body = new byte[inputMessage.getBody().available()];

inputMessage.getBody().read(body);

// 转化为字符串
String bodyStr = new String(body);

// 转换为对象
JSONObject bodyObj = JSONObject.parseObject(bodyStr);

// 定义无需解密参数
List<String> noDecryptFiled = Arrays.asList("appClient", "channel", "version", "token");

// 定义解密后参数map
HashMap<String, String> decryptParam = new HashMap<>();

// 循环请求对象
for (Map.Entry<String, Object> stringObjectEntry : bodyObj.entrySet()) {

String key = stringObjectEntry.getKey();
String value = stringObjectEntry.getValue().toString();

// 如果是开发环境,无需解密
if (Result.getEnv().equals("dev")) {
decryptParam.put(key, value);
continue;
}

// 若是无需解密参数,直接put进decryptParam
if (noDecryptFiled.contains(stringObjectEntry.getKey())) {
decryptParam.put(key, value);
continue;
}

// 解密
decryptParam.put(key, AESUtil.decrypt(value, Result.SALT));
}

// 转换为byte
byte[] decrypt = JSON.toJSONString(decryptParam).getBytes(StandardCharsets.UTF_8);

final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return bais;
}

@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};

} catch (Exception e) {
throw new BaseException(SystemErrorType.BUSINESS_ERROR, "参数解密失败,请检查");
}

}
}
  • 首先大家注意,DecryptRequest 类我们没有直接实现 RequestBodyAdvice 接口,而是继承自 RequestBodyAdviceAdapter 类,该类是 RequestBodyAdvice 接口的子类,并且实现了接口中的一些方法,这样当我们继承自 RequestBodyAdviceAdapter 时,就只需要根据自己实际需求实现某几个方法即可。
  • supports:该方法用来判断哪些接口需要处理接口解密,我们这里的判断逻辑是方法上或者参数上含有 @Decrypt 注解的接口,处理解密问题。
  • beforeBodyRead:这个方法会在参数转换成具体的对象之前执行,我们先从流中加载到数据,然后对数据进行解密,解密完成后再重新构造 HttpInputMessage 对象返回。

参考文章

Spring Boot 接口参数加密解密的实现方法