个人博客


在一些业务场景中,需要对http的请求体和响应体做加解密的操作,如果在controller中来调用加解密函数,会增加代码的耦合度,同时也会增加调试的难度。

参考spring中http请求的链路,选择过滤器来对请求和响应做加解密的调用。只需要在过滤器中对符合条件的url做拦截处理即可。

一般在过滤器中修改请求体和响应体,以往需要自行创建Wrapper包装类,从原请求Request对象中读取原请求体,修改后重新放入新的请求对象中等等操作……非常麻烦。如果可以在过滤器中只定义加解密的函数,然后调用一个API传入这些加解密函数,中间操作统统不管,这样用起来岂不是更爽!

1、启动类配置注解

新增注解@ServletComponentScan

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan
public class HttpdecryptApplication {
public static void main(String[] args) {
SpringApplication.run(HttpdecryptApplication.class, args);
}
}

2、过滤器实现

2.1、用Base64算法做加解密示例

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
@WebFilter(urlPatterns = {"/decrypt/*"}, filterName = "decryptFilter")
@Slf4j
public class DecryptFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/**
* 1.原请求/响应对象强转
*/
HttpServletRequest originalRequest = (HttpServletRequest) request;
HttpServletResponse originalResponse = (HttpServletResponse) response;

/**
* 2.读取原请求体(密文),执行修改请求体函数得到修改后的请求体(明文),然后构建新的请求对象(包含修改后的请求体)
*/
String originalRequestBody = ServletUtil.readRequestBody(originalRequest); // 读取原请求体(密文)
String modifyRequestBody = this.decryptBody(originalRequestBody); // 修改请求体(明文)
HttpServletRequest orginalRequest = (HttpServletRequest) request;
ModifyRequestBodyWrapper requestWrapper = new ModifyRequestBodyWrapper(orginalRequest, modifyRequestBody);

/**
* 3.构建新的响应对象,执行调用链(用新的请求对象和响应对象)
* 得到应用层的响应后(明文),执行修改响应体函数,最后得到需要响应给调用方的响应体(密文)
*/
ModifyResponseBodyWrapper responseWrapper = new ModifyResponseBodyWrapper(originalResponse);
chain.doFilter(requestWrapper, responseWrapper);
String originalResponseBody = responseWrapper.getResponseBody(); // 原响应体(明文)
String modifyResponseBody = this.encryptBody(originalResponseBody); // 修改后的响应体(密文)

/**
* 4.将修改后的响应体用原响应对象的输出流来输出
* 要保证响应类型和原请求中的一致,并重新设置响应体大小
*/
originalResponse.setContentType(requestWrapper.getOrginalRequest().getContentType()); // 与请求时保持一致
byte[] responseData = modifyResponseBody.getBytes(responseWrapper.getCharacterEncoding()); // 编码与实际响应一致
originalResponse.setContentLength(responseData.length);
@Cleanup ServletOutputStream out = originalResponse.getOutputStream();
out.write(responseData);
}

/**
* 解密函数,用Base64进行解密
*
* @param originalBody 加密的请求体(密文)
* @return
*/
private String decryptBody(String originalBody) {
return Base64.decodeToString(originalBody);
}

/**
* 加密函数,用Base64进行加密
*
* @param originalBody 需要加密的响应体(明文)
* @return
*/
private String encryptBody(String originalBody) {
return Base64.encodeToString(originalBody);
}
}

使用步骤

  1. 实现Filter接口。
  2. 使用@WebFilter注解指定拦截的url,可以配置多个url。

处理逻辑

  1. 从servlet中读取原请求体(密文)。
  2. 调用解密函数获得明文。
  3. 构建新的请求对象,包装修改后的请求体(明文)。
  4. 构建新的响应对象,调用链调用应用层获得响应。
  5. 从新的响应对象中获得响应体(明文)。
  6. 调用加密函数对响应体进行加密。
  7. 用原响应对象的输出流,将加密后的密文响应体输出。

函数中使用的请求包装类ModifyRequestBodyWrapper和响应包装类ModifyResponseBodyWrapper在文末附录中贴出,可以直接copy到项目工程中使用。

3、测试验证

1
2
3
4
5
6
7
8
9
10
@RestController
@Slf4j
@RequestMapping("/decrypt")
public class WebController {
@PostMapping("/test")
public String test(@RequestBody String requestBody) {
log.info("经过解密后的数据:{}", requestBody);
return "success-交易成功";
}
}
1
2
3
4
5
6
7
8
9
10
11
public class HttpdecryptApplicationTests {
@Test
public void test() {
HttpResponse response = HttpRequest
.post("http://127.0.0.1:10400/decrypt/test")
.body("eyJlbmNyeXB0SW5mbyI6IuWKoOWvhuaVsOaNriIsInZlcnNpb24iOiIxLjAifQ==")
.send();
String result = response.bodyText();
System.out.println(Base64.decodeToString(result)); // success-交易成功
}
}

4、优化改进

以上就是以往的处理方式;对于过滤器中的处理逻辑,如果项目中做不同的加解密每次都要这样去实现,未免有些冗余。

重新分析不难发现在过滤器中的处理逻辑始终都是不变的,对于不同的加解密方式只有加解密函数是变化的。为此可以引入函数式编程的方式,对于处理逻辑进行封装,每次只需要定义不同的加解密函数然后调用封装好的API即可。

改进后的过滤器

1
2
3
4
5
6
7
8
9
10
@WebFilter(urlPatterns = {"/decrypt/*"}, filterName = "decryptFilter")
@Slf4j
public class DecryptFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Function<String, String> modifyRequestBodyFun = Base64::decodeToString; // 解密函数
Function<String, String> modifyResponseBodyFun = Base64::encodeToString; // 加密函数
HttpUtil.modifyHttpData(request, response, chain, modifyRequestBodyFun, modifyResponseBodyFun);
}
}
  1. 只需要在过滤器上配置需要拦截的url列表、定义加解密函数然后调用封装好的API即可。
  2. 过滤器中不会改变请求和响应的字符集,都是沿用原来的。
  3. 只能针对于带有请求体的请求做加解密处理。
  4. 另外modifyHttpData函数有另外的重载,支持修改Content-Type

HttpUtil也在文末附录中贴出,直接copy到项目工程中使用。

对于函数式编程不熟悉的同学可以去学习下Java中如何使用 lambda 表达式和Java的几种内置的函数接口(JDK1.8版本及以上才支持);上面的lambda 表达式其实是一种简写的方式,还可以用其最一般化的方式来表示。

1
2
3
4
5
6
Function<String, String> modifyRequestBodyFun = (originalBody) -> {
return Base64.decodeToString(originalBody);
};
Function<String, String> modifyResponseBodyFun = (originalBody) -> {
return Base64.encodeToString(originalBody);
};

参考链接

代码地址

附录

请求包装类

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* 修改http请求体和contentType后构建新的请求对象
* 只针对请求体可读的请求类型
*
* @author zhaoxb
* @create 2019-09-26 17:49
*/
@Data
public class ModifyRequestBodyWrapper extends HttpServletRequestWrapper {
/**
* 原请求对象
*/
private HttpServletRequest orginalRequest;
/**
* 修改后的请求体
*/
private String modifyRequestBody;
/**
* 修改后的请求类型
*/
private String contentType;

/**
* 修改请求体,请求类型沿用原来的
*
* @param orginalRequest 原请求对象
* @param modifyRequestBody 修改后的请求体
*/
public ModifyRequestBodyWrapper(HttpServletRequest orginalRequest, String modifyRequestBody) {
this(orginalRequest, modifyRequestBody, null);
}

/**
* 修改请求体和请求类型
*
* @param orginalRequest 原请求对象
* @param modifyRequestBody 修改后的请求体
* @param contentType 修改后的请求类型
*/
public ModifyRequestBodyWrapper(HttpServletRequest orginalRequest, String modifyRequestBody, String contentType) {
super(orginalRequest);
this.modifyRequestBody = modifyRequestBody;
this.orginalRequest = orginalRequest;
this.contentType = contentType;
}

/**
* 构建新的输入流,在新的输入流中放入修改后的请求体(使用原请求中的字符集)
*
* @return 新的输入流(包含修改后的请求体)
*/
@Override
@SneakyThrows
public ServletInputStream getInputStream() {
return new ServletInputStream() {
private InputStream in = new ByteArrayInputStream(modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()));

@Override
public int read() throws IOException {
return in.read();
}

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {

}
};
}

/**
* 获取新的请求体大小
*
* @return
*/
@Override
@SneakyThrows
public int getContentLength() {
return modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()).length;
}

/**
* 获取新的请求体大小
*
* @return
*/
@Override
@SneakyThrows
public long getContentLengthLong() {
return modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()).length;
}

/**
* 获取新的请求类型,默认沿用原请求的
*
* @return
*/
@Override
public String getContentType() {
return StringUtils.isBlank(contentType) ? orginalRequest.getContentType() : contentType;
}

/**
* 修改contentType
*
* @param name 请求头
* @return
*/
@Override
public Enumeration<String> getHeaders(String name) {
if (null != name && name.replace("-", "").toLowerCase().equals("contenttype") && !StringUtils.isBlank(contentType)) {
return new Enumeration<String>() {
private boolean hasGetted = false;

@Override
public boolean hasMoreElements() {
return !hasGetted;
}

@Override
public String nextElement() {
if (hasGetted) {
throw new NoSuchElementException();
} else {
hasGetted = true;
return contentType;
}
}
};
}
return super.getHeaders(name);
}
}

响应包装类

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
110
111
112
113
114
115
116
117
118
/**
* 构建新的响应对象,缓存响应体
* 可以通过此对象获取响应体,然后进行修改,通过原响应流返回给调用方
*
* @author zhaoxb
* @create 2019-09-26 17:52
*/
@Data
public class ModifyResponseBodyWrapper extends HttpServletResponseWrapper {
/**
* 原响应对象
*/
private HttpServletResponse originalResponse;
/**
* 缓存响应体的输出流(低级流)
*/
private ByteArrayOutputStream baos;
/**
* 输出响应体的高级流
*/
private ServletOutputStream out;
/**
* 输出响应体的字符流
*/
private PrintWriter writer;

/**
* 构建新的响应对象
*
* @param resp 原响应对象
*/
@SneakyThrows
public ModifyResponseBodyWrapper(HttpServletResponse resp) {
super(resp);
this.originalResponse = resp;
this.baos = new ByteArrayOutputStream();
this.out = new SubServletOutputStream(baos);
this.writer = new PrintWriter(new OutputStreamWriter(baos));
}

/**
* 获取输出流
*
* @return
*/
@Override
public ServletOutputStream getOutputStream() {
return out;
}

/**
* 获取输出流(字符)
*
* @return
*/
@Override
public PrintWriter getWriter() {
return writer;
}

/**
* 获取响应体
*
* @return
* @throws IOException
*/
public String getResponseBody() throws IOException {
return this.getResponseBody(null);
}

/**
* 通过指定字符集获取响应体
*
* @param charset 字符集,指定响应体的编码格式
* @return
* @throws IOException
*/
public String getResponseBody(String charset) throws IOException {
/**
* 应用层会用ServletOutputStream或PrintWriter字符流来输出响应
* 需要把这2个流中的数据强制刷到ByteArrayOutputStream这个流中,否则取不到响应数据或数据不完整
*/
out.flush();
writer.flush();
return new String(baos.toByteArray(), StringUtils.isBlank(charset) ? this.getCharacterEncoding() : charset);
}

/**
* 输出流,应用层会用此流来写出响应体
*/
class SubServletOutputStream extends ServletOutputStream {
private ByteArrayOutputStream baos;

public SubServletOutputStream(ByteArrayOutputStream baos) {
this.baos = baos;
}

@Override
public void write(int b) {
baos.write(b);
}

@Override
public void write(byte[] b) {
baos.write(b, 0, b.length);
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setWriteListener(WriteListener writeListener) {

}
}
}

HttpUtil封装工具类

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
@Slf4j
public class HttpUtil {
/**
* 修改http请求体/响应体
*
* @param originalRequest 原请求对象
* @param originalResponse 原响应对象
* @param chain 调用链
* @param modifyRequestBodyFun 修改请求体函数
* @param modifyResponseBodyFun 修改响应体函数
* @throws IOException
* @throws ServletException
*/
public static void modifyHttpData(ServletRequest originalRequest, ServletResponse originalResponse, FilterChain chain,
Function<String, String> modifyRequestBodyFun, Function<String, String> modifyResponseBodyFun) throws IOException, ServletException {
modifyHttpData(originalRequest, originalResponse, chain, modifyRequestBodyFun, modifyResponseBodyFun, null);
}

/**
* 修改http请求体/响应体
*
* @param request 原请求对象
* @param response 原响应对象
* @param chain 调用链
* @param modifyRequestBodyFun 修改请求体函数
* @param modifyResponseBodyFun 修改响应体函数
* @param requestContentType 修改后的请求类型
* @throws IOException
* @throws ServletException
*/
public static void modifyHttpData(ServletRequest request, ServletResponse response, FilterChain chain,
Function<String, String> modifyRequestBodyFun, Function<String, String> modifyResponseBodyFun,
String requestContentType) throws IOException, ServletException {
/**
* 1.原请求/响应对象强转
*/
HttpServletRequest originalRequest = (HttpServletRequest) request;
HttpServletResponse originalResponse = (HttpServletResponse) response;

/**
* 2.读取原请求体(密文),执行修改请求体函数得到修改后的请求体(明文),然后构建新的请求对象(包含修改后的请求体)
*/
String originalRequestBody = ServletUtil.readRequestBody(originalRequest); // 读取原请求体(密文)
String modifyRequestBody = modifyRequestBodyFun.apply(originalRequestBody); // 修改请求体(明文)
ModifyRequestBodyWrapper requestWrapper = modifyRequestBodyAndContentType(originalRequest, modifyRequestBody, requestContentType);

/**
* 3.构建新的响应对象,执行调用链(用新的请求对象和响应对象)
* 得到应用层的响应后(明文),执行修改响应体函数,最后得到需要响应给调用方的响应体(密文)
*/
ModifyResponseBodyWrapper responseWrapper = getHttpResponseWrapper(originalResponse);
chain.doFilter(requestWrapper, responseWrapper);
String originalResponseBody = responseWrapper.getResponseBody(); // 原响应体(明文)
String modifyResponseBody = modifyResponseBodyFun.apply(originalResponseBody); // 修改后的响应体(密文)

/**
* 4.将修改后的响应体用原响应对象的输出流来输出
* 要保证响应类型和原请求中的一致,并重新设置响应体大小
*/
originalResponse.setContentType(requestWrapper.getOrginalRequest().getContentType()); // 与请求时保持一致
byte[] responseData = modifyResponseBody.getBytes(responseWrapper.getCharacterEncoding()); // 编码与实际响应一致
originalResponse.setContentLength(responseData.length);
@Cleanup ServletOutputStream out = originalResponse.getOutputStream();
out.write(responseData);
}

/**
* 修改请求体
*
* @param request 原请求
* @param modifyRequestBody 修改后的请求体
* @return
*/
public static ModifyRequestBodyWrapper modifyRequestBody(ServletRequest request, String modifyRequestBody) {
return modifyRequestBodyAndContentType(request, modifyRequestBody, null);
}

/**
* 修改请求体和请求类型
*
* @param request 原请求
* @param modifyRequestBody 修改后的请求体
* @param contentType 请求类型
* @return
*/
public static ModifyRequestBodyWrapper modifyRequestBodyAndContentType(ServletRequest request, String modifyRequestBody, String contentType) {
log.debug("ContentType改为 -> {}", contentType);
HttpServletRequest orginalRequest = (HttpServletRequest) request;
return new ModifyRequestBodyWrapper(orginalRequest, modifyRequestBody, contentType);
}

/**
* 用原响应对象来构建新的http响应包装对象
*
* @param response 原响应对象
* @return
*/
public static ModifyResponseBodyWrapper getHttpResponseWrapper(ServletResponse response) {
HttpServletResponse originalResponse = (HttpServletResponse) response;
return new ModifyResponseBodyWrapper(originalResponse);
}
}