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

在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. SQL server 数据库巡检

    SELECT name FROM sysobjects where xtype='u' and name <>'XzryGzGrant' AND name LIKE 'XzryGzGran ...

  2. 在 Visual Studio 中规范化文件编辑

    1 配置文件存放 生成了对应的 .editorconfig 文件,存放在仓储的根目录.即对整个仓储所有的用 VS 作为 IDE 编辑的项目生效. 同时支持子目录有自己的 .editorconfig 文 ...

  3. js原型,原型链(不断补充中)

    1.如何使用构造器? function Person(name, age) { this.name = name; this.age = age; } var man = new Person(&qu ...

  4. 十、Doris操作参考手册

    1.SQL参考 1.1  用户账户管理 1.2  集群管理 1.3  DDL 1.4  DML 2.函数参考 2.1  日期函数 2.2  字符串函数 2.3  聚合函数 2.4  Cast转换函数 ...

  5. WEB服务与NGINX(24)- LNMP架构部署wordpress

    目录 1. LNMP架构项目实战 1.1 LNMP架构介绍 1.2 LNMP架构部署wordpress 1.2.1 LNMP环境介绍 1.2.2 二进制部署mariadb 1.2.3 部署php-fp ...

  6. fastposter v2.8.1 发布 电商海报生成器

    fastposter v2.8.1 发布 电商海报生成器 fastposter海报生成器,电商海报编辑器,电商海报设计器,fast快速生成海报 海报制作 海报开发.二维码海报,图片海报,分享海报,二维 ...

  7. ssl协议存在弱加密算法修复,禁用低版本的TLS

    验证用网站:https://www.ssleye.com/ssltool/cipher_suites.html https://www.site24x7.com/zhcn/tools/tls-chec ...

  8. WPF绑定数据源到ListBox等selector的注意事项

    如果使用CollectionViewSource绑定到控件上,会导致默认选择第一项,而使用List,SelectedItem就默认为空. 要避免默认选择第一项,就要设置 ListBox.IsSynch ...

  9. linux用户管理:创建用户,删除用户,管理用户,用户配置

    目录 一.关于用户 二.用户的三种类型 三.与用户有关的配置文件详解 四.创建用户 五.设置用户密码 六.删除用户 七.用户密码时效管理 八.查看用户相关信息的命令 九.修改用户基本信息 十.管理用户 ...

  10. RabbitMQ系列(五) RabbitMQ的文件和目录位置

    概述 每个RabbitMQ节点使用一些文件和目录,用于加载配置.存储数据 / 元数据 / 日志文件等等.这些文件和目录的位置是可以自定义的. 本指南涵盖: 1)如何自定义RabbitMQ节点所使用的各 ...