在一些业务场景中,需要对http的请求体和响应体做加解密的操作,如果在controller中来调用加解密函数,会增加代码的耦合度,同时也会增加调试的难度。
参考spring中http请求的链路,选择过滤器来对请求和响应做加解密的调用。只需要在过滤器中对符合条件的url做拦截处理即可。
一般在过滤器中修改请求体和响应体,以往需要自行创建Wrapper包装类,从原请求Request对象中读取原请求体,修改后重新放入新的请求对象中等等操作……非常麻烦。如果可以在过滤器中只定义加解密的函数,然后调用一个API传入这些加解密函数,中间操作统统不管,这样用起来岂不是更爽!
1、启动类配置注解
新增注解@ServletComponentScan
@SpringBootApplication
@ServletComponentScan
public class HttpdecryptApplication {
public static void main(String[] args) {
SpringApplication.run(HttpdecryptApplication.class, args);
}
}
2、过滤器实现
2.1、用Base64算法做加解密示例
代码语言:javascript复制@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);
}
}
使用步骤:
- 实现
Filter
接口。 - 使用
@WebFilter
注解指定拦截的url,可以配置多个url。
处理逻辑
- 从servlet中读取原请求体(密文)。
- 调用解密函数获得明文。
- 构建新的请求对象,包装修改后的请求体(明文)。
- 构建新的响应对象,调用链调用应用层获得响应。
- 从新的响应对象中获得响应体(明文)。
- 调用加密函数对响应体进行加密。
- 用原响应对象的输出流,将加密后的密文响应体输出。
函数中使用的请求包装类ModifyRequestBodyWrapper
和响应包装类ModifyResponseBodyWrapper
在文末附录中贴出,可以直接copy到项目工程中使用。
3、测试验证
代码语言:javascript复制@RestController
@Slf4j
@RequestMapping("/decrypt")
public class WebController {
@PostMapping("/test")
public String test(@RequestBody String requestBody) {
log.info("经过解密后的数据:{}", requestBody);
return "success-交易成功";
}
}
代码语言:javascript复制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即可。
改进后的过滤器
代码语言:javascript复制@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);
}
}
- 只需要在过滤器上配置需要拦截的url列表、定义加解密函数然后调用封装好的API即可。
- 过滤器中不会改变请求和响应的字符集,都是沿用原来的。
- 只能针对于带有请求体的请求做加解密处理。
- 另外
modifyHttpData
函数有另外的重载,支持修改Content-Type
。
HttpUtil
也在文末附录中贴出,直接copy到项目工程中使用。
对于函数式编程不熟悉的同学可以去学习下Java中如何使用 lambda
表达式和Java的几种内置的函数接口(JDK1.8版本及以上才支持);上面的lambda
表达式其实是一种简写的方式,还可以用其最一般化的方式来表示。
Function<String, String> modifyRequestBodyFun = (originalBody) -> {
return Base64.decodeToString(originalBody);
};
Function<String, String> modifyResponseBodyFun = (originalBody) -> {
return Base64.encodeToString(originalBody);
};
参考链接
- SpringBoot框架中,使用过滤器进行加密解密操作
代码地址
- github:https://github.com/senlinmu1008/spring-boot/tree/master/httpdecrypt
- gitee:https://gitee.com/ppbin/spring-boot/tree/master/httpdecrypt
附录
请求包装类
代码语言:javascript复制/**
* 修改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);
}
}
响应包装类
代码语言:javascript复制/**
* 构建新的响应对象,缓存响应体
* 可以通过此对象获取响应体,然后进行修改,通过原响应流返回给调用方
*
* @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封装工具类
代码语言:javascript复制@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);
}
}