文章目录
本文不仅介绍了body内容修改后如何传递,也介绍了get请求 在修改内容后如何继续传递。
【原创作者 csdn: 孟秋与你】
解密部分
核心思路
-
拦截每次请求 所以要么在拦截器 要么在过滤器中做 (正常来说 其实只能在过滤器做)
-
修改request中的参数
-
把修改后的参数设置回去(难点)
一句话来说就是拦截request并修改传递
spring&servelt基础
每个http请求都是访问了一个servlet,servlet有过滤器filter的概念。
在spring框架中 其实就是封装了一个DispatcherServlet,并且实现了各种过滤器和拦截器。
在过滤器中,有很重要的一个步骤:
传递过滤链,用通俗的话解释就是 当前过滤器的一顿操作 不能阻碍了其它过滤器的执行,所以需要将request和response传递给下一个过滤器
filterChain.doFilter(request, response);
所以我们在过滤器中进行解密,并将request的值修改后传递,这样可以保证每个过滤器拿到的都是解密后的值。
如果是在拦截器里面做,那可能在过滤器就出现了报错(取决于过滤器自定义的功能复不复杂 有没有涉及参数解密), 我们避免节外生枝 本文都是在过滤器里面进行解密。
此外 全局参数解密不适合实现HandlerMethodArgumentResolver接口来实现 :
- 只拦截get query(form表单提交)请求
- 参数类型不好控制
核心接口类
-
HttpServletRequestWrapper
这里补充一个基础知识,流读了一遍之后就不能再读取了,所以需要通过这个包装类,使得流可以重复使用 -
ServletInputStream
与HttpServletRequestWrapper的子类配合使用 重写读取流的方法 -
OncePerRequestFilter
拦截每次请求的过滤器
核心代码
-
重写getInputStream getReader 方法 用于修改post请求(body)
getInputStream 方法修改流,getReader 方法获取修改后的流 -
重写getParameterValues方法 ,重写getParameterMap方法 用于修改get请求(query请求 即form表单提交的请求)
其中getParameterMap方法可以不重写 直接改
-
重写getAttribute方法 用于修改get请求 (PathVariables类型的请求)
(注:本文代码是demo 不推荐复制即用,最好需要有自己的理解 在后文会解释为什么代码这么写)
@Configuration
public class DecryptRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 包装请求对象
CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(request);
// 继续过滤链,使用包装后的请求对象
chain.doFilter(wrappedRequest, response);
}
}
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.apache.catalina.util.ParameterMap;
import org.springframework.web.servlet.HandlerMapping;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
private final Map<String, String[]> params = new HashMap<>();
private final Map<String, String> pathVariables = new HashMap<>();
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取并缓存请求体内容
String s = new String(readBytes(request.getInputStream()), StandardCharsets.UTF_8);
// 这里模拟的body解密
String replace = s.replace("孟秋", "大孟秋");
body = replace.getBytes(StandardCharsets.UTF_8);
ParameterMap<String, String[]> parameterMap = (ParameterMap<String, String[]>) request.getParameterMap();
parameterMap.setLocked(false);
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
// 取值
String[] value = entry.getValue();
String s1 = value[0];
// 模拟解密
parameterMap.put(entry.getKey(), new String[]{"解密参数"});
}
parameterMap.setLocked(true);
}
private byte[] readBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
}
private Map<String, String[]> decryptParams(Map<String, String[]> parameterMap) {
// 解密逻辑
return parameterMap; // 示例
}
@Override
public String getParameter(String name) {
String[] paramArray = params.get(name);
return paramArray != null && paramArray.length > 0 ? paramArray[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public String[] getParameterValues(String name) {
String[] parameterValues = super.getParameterValues(name);
// 模拟get请求(query请求 即form表单提交)的解密
return new String[]{"加密"};
}
@Override
public Enumeration<String> getAttributeNames() {
return super.getAttributeNames();
}
@Override
public Object getAttribute(String name) {
if (Objects.equals(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, name)) {
Map<String,String> map = (Map) super.getAttribute(name);
for (Map.Entry<String, String> entry : map.entrySet()) {
// 模拟解密
return Map.of(entry.getKey(), entry.getValue() + "解密");
}
}
// 其它节点按照原逻辑
return super.getAttribute(name);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
String s = new String(body, StandardCharsets.UTF_8);
System.out.println(s);
return new CachedBodyServletInputStream(body);
}
@Override
public String getRequestURI() {
String uri = super.getRequestURI();
for (Map.Entry<String, String> entry : pathVariables.entrySet()) {
uri = uri.replace("{" + entry.getKey() + "}", entry.getValue());
}
return uri;
}
}
body解密核心原理讲解
核心:
- request.getInputStream的流缓存起来
- 改写缓存流的内容 实现解密
- 重写了HttpServletRequestWrapper类的getInputStream方法 该方法就是获取我们缓存起来的流 这样在controller层 @RequestBody获取的就是我们的缓存流(解密后的流)对应的内容
get解密核心原理讲解
get query请求讲解
get比body难一些,踩的坑也不少
先将参数取出来
(代码片段截取的上文代码)
ParameterMap<String, String[]> parameterMap = (ParameterMap<String, String[]>) request.getParameterMap();
如果这个时候我们直接遍历map 并重新put修改值 是会报错的
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
// 取值
String[] value = entry.getValue();
String s1 = value[0];
// 这里是模拟解密
parameterMap.put(entry.getKey(), new String[]{"解密参数"});
}
会报错不支持操作 not supportException,如果这时候放弃就大意了,我们跟进源码看看为什么抛异常:
可以看到 如果locked 则会抛出异常,而它又提供了一个public的setLocked的方法
所以这就是为什么代码中 会有这一行
parameterMap.setLocked(false);
我们先把锁打开后,往map里面修改了内容,还需要把锁关上, 我们看ParameterMap类的源码也能看到 它有很多地方是通过判断是否锁了 我们不能改变原来的逻辑。
// 修改完map后重新上锁
parameterMap.setLocked(true);
如果我们的controller是 :
@GetMapping
public void test(Request request)
那么直接request.getParameterMap就可以获取解密后的参数了
众所周知 我们都用了spring/springboot 早就不太可能这么写丑陋代码了,更多时候是如下:
@GetMapping
public void test(String name)
这个时候会发现 明明reqeust的parameterMap被修改了,但是name参数竟然还是原始的值!
这个时候就需要打个断点调试了,博主经过调试后发现 最后是动态代理传的参改变了,servlet到controller层的过程中 虽然一开始确实是传的reqeust 但在某一步时 它去获取了parameterValues !
parameterMap变量位于org.apache.catalina.connector.Request类
而parameterValues变量位于org.apache.tomcat.util.http.Parameters类
(上面两个源码类都是基于springboot3版本自带的tomcat包,springboot2可能会有差异,博主推测都可以从RequestFacade类作为入口看)
所以我们需要在HttpServletRequestWrapper的子类 CustomHttpServletRequestWrapper 中重写parameterValues方法
(避免数据不一致情况下 我们map和values方法都将解密数据放进去)
get pathVariables请求讲解
@PathVariables 风格的请求,一般用于restful风格中的detail 、delete接口,参数通过以下方式来获取
(Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)
这个map是不可修改的,并没有和parameterMap一样提供后门;
它的准确类型是 java.util.Collections类中的内部私有类UnmodifiableMap
既然不能直接修改,我们就重写getAttribute方法 , 因为原始Map不能被修改 我们返回一个新的Map回去就好了
(tips: Map.of是jdk9的特性,jdk版本不够的话 和自己new一个不可修改的Map是没什么区别的 )
@Override
public Object getAttribute(String name) {
if (Objects.equals(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, name)) {
Map<String,String> map = (Map) super.getAttribute(name);
for (Map.Entry<String, String> entry : map.entrySet()) {
// 模拟解密
return Map.of(entry.getKey(), entry.getValue() + "解密");
}
}
// 其它节点按照原逻辑
return super.getAttribute(name);
}
这时候我们可能会有疑问,原始Map是Collections.UnmodifiableMap 类型的(Map的一个子类), 那我们new一个别的类型的Map回去会不会有问题呢
A:不会有问题 , 只是为了防止数据被修改 用的不可修改的一个map存放而已
我们的Map.of创建的同样也是不允许被修改的map ,
如果实在不放心 我们new一个hashMap 存放Collections.unmodifiableMap类型回去
for (Map.Entry<String, String> entry : map.entrySet()) {
// 模拟解密
Map<String, String> res = new HashMap<>();
res.put(entry.getKey(), entry.getValue()+"解密");
return Collections.unmodifiableMap(res);
}
源码里面会把我们的hashMap 变成 Collections.unmodifiableMap类型的map
但是这里用到了一个反射,在拦截器里用反射意味着性能会下降很多,对TPS/QPS要求严格的项目来说 用Collections.unmodifiableMap包装回去 实在是没必要的操作 (普通项目爱怎么写都一样)
效果示例:
总结
经过简单的源码分析 可以看到 :
- 我们要的大部分参数 如parameterMap,attributes
(本质是个ConcurrentHashMap) 都是在Request类
-
有个很特殊的变量 parameterValues 在Parameters类,这个变量决定我们controller层get请求接口中的参数
(全路径org.apache.tomcat.util.http.Parameters)
-
不管是get还是post请求,对外提供的方法入口都是 RequestFacade门面类 (设计模式中的门面模式) , 我们重写的RequestWrapper类里面的this.request 指的也是requestFacade的实例对象;
我们要修改什么值 重写HttpServletRequestWrapper的对应方法即可 -
再次提醒 ,本文基于的是springboot 3.3.1版本自带的tomcat版本进行源码调试
如果其它版本tomcat没有对应路径 ,建议从RequestFacade类打断点调试追踪 它作为一个入口类 可以追踪到大部分我们要的信息;博主可以肯定 在旧版本的tomcat也有RequestFacade类
加密部分
一句话来说就是拦截返回值
我们或许知道HandlerMethodReturnValueHandler 接口,但请注意 在全局加密并返回给前端的场景下,在WebMvcConfigurer实现类中重写addReturnValueHandlers方法是无效的,原因是会被默认配置覆盖,需要通过RequestMappingHandlerAdapter 取出解析器,把默认的RequestResponseBodyMethodProcessor替换
具体可以看博主另一博客
https://blog.csdn.net/qq_36268103/article/details/136292719?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22136292719%22%2C%22source%22%3A%22qq_36268103%22%7D#_533
我们采用的也是上面博客中提到的 实现ResponseBodyAdvice接口,并加上注解的形式
(原理也在上面链接博客有提及 感兴趣可以查看)
通过ResponseBodyAdvice方式
import com.qiuhuanhen.springboot3demo.bean.result.Result;
import com.qiuhuanhen.springboot3demo.encrypt.EncryptUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.AbstractMap;
import java.util.Objects;
@Order(Integer.MIN_VALUE+1)
@ControllerAdvice
public class CustomReturnValueHandler implements ResponseBodyAdvice<Result> {
/**
* 反射缓存池 避免每次都反射, 替换成自己的返回类
*/
public static AbstractMap.SimpleEntry<Class, Class> parameterTypeCache = new AbstractMap.SimpleEntry<>(Result.class, Result.class);
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return Objects.equals(parameterTypeCache.getValue(), returnType.getParameterType());
}
@Override
public Result beforeBodyWrite(Result body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 项目返回格式为 Result.success(data) 有工作经验应该都看得懂
if (body instanceof Result) {
if (Objects.nonNull(body.getData())) {
// Encrypt the data 替换成自己的加密
String encryptedData = EncryptUtils.encrypt(body.getData().toString(), EncryptUtils.getKey());
String string = encryptedData.toString();
body.setData(string);
}
// If data is null, return the original result
return body;
}
// If the body is not of type Result, return it as is
return body;
}
}
弊端
此时假设我们data返回的是一个PeopleVO
(即 Result.success(new PeopleVO))
@Data
public class PeopleVO{
private String name;
private Integer age;
}
那么前端解密出来的字符串就是
“PeopleVO(name=xxx,age=xxx)” 而不是json字符串,对解析会造成较大的影响。
通过HttpMessageConverter方式
HttpMessageConverter是在ResponseBodyAdvice之后执行的,spring将实体类转换json的操作 也是通过jackson来实现的;
默认通过AbstractJackson2HttpMessageConverter类的writeInternal方法实现 (其实是子类MappingJackson2HttpMessageConverter)
那我们只需要添加一个自定义的HttpMessageConverter类 且顺序位于默认的MappingJackson2HttpMessageConverter之前就好了
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
/** 还有个configureMessageConverters方法 这个方法是会替换默认的转换器 **/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 位于list最上面
converters.add(0,new EncryptHttpMessageConverter());
}
}
博主一开始继承错了类 半天不进入自定义转换器,调源码调半天后发现核心是需要canWrite为true, 如果某天发现自定义转换器不进入断点 那大概率就是canWrite结果为false了
(以下代码注释部分可以不看 是博主的一个笔记,下面代码是可以直接复制使用的)
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiuhuanhen.springboot3demo.encrypt.EncryptUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import java.io.IOException;
import java.lang.reflect.Type;
public class EncryptHttpMessageConverter extends MappingJackson2HttpMessageConverter {
/**
* 如果继承了MappingJackson2HttpMessageConverter的父类 需要将 application/json 传给了 canWrite 方法 ,使得 canWrite 为true ;
* {@link MappingJackson2HttpMessageConverter#MappingJackson2HttpMessageConverter(ObjectMapper)} 参照构造函数的写法
* {@link this#canWrite(Class, MediaType)} 总之就是canWrite方法为true的转换器才会被使用
* 拦截canWrite 为false的代码:
* {@link org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)} } 源码路径
* 校验代码:if (((GenericHttpMessageConverter)converter).canWrite((Type)targetType, valueType, selectedMediaType))
*/
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(object);
JSONObject jsonObject = JSONObject.parseObject(json);
// 替换成自己的json解析
if (jsonObject.containsKey("data") && ObjectUtil.isNotEmpty(jsonObject.get("data")) ) {
// Encrypt the entire JSON string
String encryptedJson = EncryptUtils.encrypt(JSON.toJSONString(jsonObject.get("data")), EncryptUtils.getKey());
jsonObject.put("data", encryptedJson);
// Write the encrypted JSON to the output message
outputMessage.getBody().write(jsonObject.toString().getBytes());
return;
}
super.writeInternal(object, type, outputMessage);
}
}
响应示例结果: