客户端、服务端网络通信,为了安全,会对报文数据进行加解密操作。

在SpringBoot项目中,最好使用参考AOP思想,加解密与Controller业务逻辑解耦,互不影响。

以解密为例:需要在request请求到达Controller之前进行拦截,获取请求body中的密文并对其进行解密,然后把解密后的明文重新设置到request的body上。

拦截器、过滤器、Controller之间的关系

如图所示,在Request对象进入Controller之前,可使用Filter过滤器或者过滤器和Interceptor拦截器进行拦截。

过滤器、拦截器主要差异:

1、过滤器来自于 Servlet;而拦截器来自于 Spring 框架;
2、触发时机不同:请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller)
3、过滤器是基于方法回调实现的;拦截器是基于动态代理(底层是反射)实现的;
4、过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中;
5、过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能;拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;

对于我们当前应用场景来说,区别就是过滤器更适用于修改request body。

为响应数据报文统一加密,也可参考上述过程实现;

具体实现分析

修改请求,会有两个问题:

1、请求体的输入流被读取,它就不能再被其他组件读取,因为输入流只能被标记、重置,并且在读取后会被消耗。

2、HttpServletRequest对象的body数据只能get,不能set,不能再次赋值。而咱们的需求是需要给HttpServletRequest body解密并重新赋值。

基于以上两个问题,咱们需要定义一个HttpServletRequest实现类,增加赋值方法,来满足我们的需求。

CustomHttpServletRequestWrapper.java

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*; /**
* 自定义HttpServletRequestWrapper
* qxc
* 20240622
*/
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper { private String body; public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) { } finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
} @Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream; } @Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
} public String getBody() {
return this.body;
} public void setBody(String body) {
this.body = body;
}
}

接下来,继续写Filter类,用于拦截请求,解析body, 解密报文,替换请求body数据。

RequestWrapperFilter.java


import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Objects; /**
* 自定义Filter
* qxc
* 20240622
*/
@Slf4j
public class RequestWrapperFilter implements Filter { @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
CustomHttpServletRequestWrapper customHttpServletRequestWrapper = null;
try {
HttpServletRequest req = (HttpServletRequest) request;
customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(req);
preHandle(customHttpServletRequestWrapper);
} catch (Exception e) {
log.warn("customHttpServletRequestWrapper Error:", e);
} chain.doFilter((Objects.isNull(customHttpServletRequestWrapper) ? request : customHttpServletRequestWrapper), response);
} public void preHandle(CustomHttpServletRequestWrapper request) throws Exception {
//仅当请求方法为POST时修改请求体
if (!request.getMethod().equalsIgnoreCase("POST")) {
return;
}
//读取原始请求体
StringBuilder originalBody = new StringBuilder();
String line;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
while ((line = reader.readLine()) != null) {
originalBody.append(line);
}
}
String bodyText = originalBody.toString();
//json字符串转成map集合
Map<String, String> map = getMap(bodyText);
//获取解密参数,解密数据
if (map != null && map.containsKey("time") && map.containsKey("data")) {
String time = map.get("time");
String key = "基于时间戳等参数生成密钥、此处请换成自己的密钥";
String data = map.get("data");
//解密数据
String decryptedData = Cipher.decrypt(key, data);
//为请求对象重新设置body
request.setBody(decryptedData);
}
} private Map<String, String> getMap(String text) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// 将JSON字符串转换为Map
Map<String, String> map = objectMapper.readValue(text, Map.class);
return map;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

AES加解密算法封装

Cipher.java

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64; import javax.crypto.spec.SecretKeySpec; /**
* 自定义AES加解密算法类
* qxc
* 20240622
*/
public class Cipher {
public static String encrypt(String key, String text){
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encryptedBytes = cipher.doFinal(text.getBytes("UTF-8"));
String cipherText = base64Encode(encryptedBytes);
return cipherText;
}catch (Exception ex){
ex.printStackTrace();
return "";
}
} public static String decrypt(String key, String cipherText) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedBytes = cipher.doFinal(base64Decode(cipherText));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}catch (Exception ex){
ex.printStackTrace();
return "";
}
} public static String getMd5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(input.getBytes());
byte[] digest = md.digest();
BigInteger bigInt = new BigInteger(1, digest);
String md5Hex = bigInt.toString(16);
while (md5Hex.length() < 32) {
md5Hex = "0" + md5Hex;
}
return md5Hex;
} catch (Exception e) {
e.printStackTrace();
return "";
}
} public static String base64Encode(byte[] bytes) {
if (bytes != null && bytes.length > 0) {
return Base64.getEncoder().encodeToString(bytes);
}
return "";
} public static byte[] base64Decode(String base64Str) {
if (base64Str != null && base64Str.length() > 0) {
return Base64.getDecoder().decode(base64Str);
}
return new byte[]{};
}
}

最后,需要在WebMvcConfigurer中配置并使用RequestWrapperFilter

import com.qxc.server.encryption.RequestWrapperFilter;
import com.qxc.server.jwt.JwtInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication
public class ServerApplication implements WebMvcConfigurer { public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
} @Bean
public FilterRegistrationBean servletRegistrationBean() {
RequestWrapperFilter userInfoFilter = new RequestWrapperFilter();
FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(userInfoFilter);
bean.setName("requestFilter");
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.LOWEST_PRECEDENCE); return bean;
} @Override
public void addInterceptors(InterceptorRegistry registry) { }
}

这样,就实现了请求报文的解密、数据替换,而且对Controller逻辑没有影响。

SpringBoot 过滤器更改 Request body ,并实现数据解密的更多相关文章

  1. 寻找写代码感觉(八)之SpringBoot过滤器的使用

    一.什么是过滤器? 过滤器是对数据进行过滤,预处理过程,当我们访问网站时,有时候会发布一些敏感信息,发完以后有的会用*替代,还有就是登陆权限控制等,一个资源,没有经过授权,肯定是不能让用户随便访问的, ...

  2. Springboot+ajax传输json数组以及单条数据的方法

    Springboot+ajax传输json数组以及单条数据的方法 下面是用ajax传输到后台单条以及多条数据的解析的Demo: 结构图如下: 下面是相关的代码: pom.xml: <?xml v ...

  3. springboot过滤器的实现

    springboot过滤器的实现 如下所示: import javax.servlet.*; import javax.servlet.annotation.WebFilter;import java ...

  4. SpringBoot中如何灵活的实现接口数据的加解密功能?

    数据是企业的第四张名片,企业级开发中少不了数据的加密传输,所以本文介绍下SpringBoot中接口数据加密.解密的方式. 本文目录 一.加密方案介绍二.实现原理三.实战四.测试五.踩到的坑 一.加密方 ...

  5. Request三种获取数据的方式

    今天在做ajax请求后台代码时,发现ajax的方法都对,但就是请求不了后台代码,后来在同事帮助下才发现前台定义了两个相同参数导致请求出错. 下面记录一下request三种获取数据的方式: 1. Req ...

  6. jplayer中动态添加列表曲目(js提取request中的list数据作为js参数使用)

    jplayer 的播放列表使用如下: $(document).ready(function(){ new jPlayerPlaylist({ jPlayer: "#jquery_jplaye ...

  7. ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除)

    原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除) ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇)   ...

  8. SpringBoot系列: 与Spring Rest服务交互数据

    不管是单体应用还是微服务应用, 现在都流行Restful风格,  下图是一个比较典型的使用rest的应用架构, 该应用不仅使用database数据源, 而且用到了一个Weather微服务, 另一方面, ...

  9. jmeter将JDBC Request查询出的数据作为下一个接口的参数

    现在有一个需求,从数据库tieba_info表查出rank小于某个值的username和count(*),然后把所有查出来的username和count(*)作为参数值,用于下一个接口. tieba_ ...

  10. javaBean默认接受request发送过来的数据,根据键自动设置属性

    javaBean默认接受request发送过来的数据,根据键自动设置属性

随机推荐

  1. 学习 Avalonia 框架笔记 如何创建一个全屏置顶的 X11 应用窗口

    本文记录我从 Avalonia 框架里面学到如何创建一个全屏置顶的 X11 应用窗口的方法 开始之前,先从 Avalonia 或 CPF 里面拷贝足够的代码,这部分代码可以从本文末尾找到下载方法 设置 ...

  2. IIncrementalGenerator 增量 Source Generator 生成代码应用 将构建时间写入源代码

    本文将和大家介绍一个 IIncrementalGenerator 增量 Source Generator 生成代码技术的应用例子,将当前的构建时间写入到代码里面.这个功能可以比较方便实现某些功能的开关 ...

  3. 优秀的 Modbus 主站(主机、客户端)仿真器、串口调试工具

    目录 优秀的 Modbus 主站(主机.客户端)仿真器.串口调试工具 主要功能 软件截图 优秀的 Modbus 主站(主机.客户端)仿真器.串口调试工具 modbus master,modbus,串口 ...

  4. 深入 Django 模型层:数据库设计与 ORM 实践指南

    title: 深入 Django 模型层:数据库设计与 ORM 实践指南 date: 2024/5/3 18:25:33 updated: 2024/5/3 18:25:33 categories: ...

  5. [Oracle故障处理]ORA-30012: undo tablespace 'UNDOTBS1' does not exist

    场景:用RMAN在异机上恢复数据. 错误信息如下: ORA-01092: ORACLE instance terminated. Disconnection forced ORA-30012: und ...

  6. Jmeter-线程组下篇

    线程组 线程组作为JMeter测试计划的核心组件之一,对于模拟并发用户的行为至关重要.线程组元件是整个测试计划的入口,所有的取样器和控制器必须放置在线程组下. 可以将线程组视为一个虚拟用户池,其中每个 ...

  7. typescript基础知识汇总

    JavaScript中所有事物(字符串.数值.数组.函数)都是对象,都有属性和方法.1.用函数定义对象,然后new对象实例.2.用Object定义并创建对象实例var o = new Object(t ...

  8. salesforce零基础学习(一百三十六)零碎知识点小总结(八)

    本篇参考: Salesforce LWC学习(七) Navigation & Toast https://developer.salesforce.com/docs/platform/lwc/ ...

  9. rsync备份服务器部署详情

    rsync  -avz --bwlimit=1024M /data/wanxhe  rsync_backup@10.x.x.38::backup/gpu007/data/ --password-fil ...

  10. C# 使用大数组内存溢出的解决办法

    在实际开发中,需要读取文件转成byte数组,文件大小四五百兆,采用win10系统,我那台电脑系统版本非常老了,一直没升级,读取文件时,就会出现OutOfMemeory异常,时不时的出现.我程序用的an ...