【springboot】springboot接口参数全局加解密,解决request内容修改后如何重新设置回去的问题

本文不仅介绍了body内容修改后如何传递,也介绍了get请求 在修改内容后如何继续传递。
【原创作者 csdn: 孟秋与你】

解密部分

核心思路

  1. 拦截每次请求 所以要么在拦截器 要么在过滤器中做 (正常来说 其实只能在过滤器做)

  2. 修改request中的参数

  3. 把修改后的参数设置回去(难点)

    一句话来说就是拦截request并修改传递

spring&servelt基础

每个http请求都是访问了一个servlet,servlet有过滤器filter的概念。

在spring框架中 其实就是封装了一个DispatcherServlet,并且实现了各种过滤器和拦截器。

在过滤器中,有很重要的一个步骤:
传递过滤链,用通俗的话解释就是 当前过滤器的一顿操作 不能阻碍了其它过滤器的执行,所以需要将request和response传递给下一个过滤器

filterChain.doFilter(request, response);

所以我们在过滤器中进行解密,并将request的值修改后传递,这样可以保证每个过滤器拿到的都是解密后的值。

如果是在拦截器里面做,那可能在过滤器就出现了报错(取决于过滤器自定义的功能复不复杂 有没有涉及参数解密), 我们避免节外生枝 本文都是在过滤器里面进行解密。

此外 全局参数解密不适合实现HandlerMethodArgumentResolver接口来实现 :

  1. 只拦截get query(form表单提交)请求
  2. 参数类型不好控制

核心接口类

  1. HttpServletRequestWrapper
    这里补充一个基础知识,流读了一遍之后就不能再读取了,所以需要通过这个包装类,使得流可以重复使用

  2. ServletInputStream
    与HttpServletRequestWrapper的子类配合使用 重写读取流的方法

  3. OncePerRequestFilter
    拦截每次请求的过滤器

核心代码

  1. 重写getInputStream getReader 方法 用于修改post请求(body)
    getInputStream 方法修改流,getReader 方法获取修改后的流

  2. 重写getParameterValues方法 ,重写getParameterMap方法 用于修改get请求(query请求 即form表单提交的请求)

    其中getParameterMap方法可以不重写 直接改

  3. 重写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解密核心原理讲解

核心:

  1. request.getInputStream的流缓存起来
  2. 改写缓存流的内容 实现解密
  3. 重写了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包装回去 实在是没必要的操作 (普通项目爱怎么写都一样)
在这里插入图片描述

效果示例:
在这里插入图片描述

总结

经过简单的源码分析 可以看到 :

  1. 我们要的大部分参数 如parameterMap,attributes
    (本质是个ConcurrentHashMap) 都是在Request类

在这里插入图片描述

  1. 有个很特殊的变量 parameterValues 在Parameters类,这个变量决定我们controller层get请求接口中的参数
    (全路径org.apache.tomcat.util.http.Parameters)
    在这里插入图片描述

  2. 不管是get还是post请求,对外提供的方法入口都是 RequestFacade门面类 (设计模式中的门面模式) , 我们重写的RequestWrapper类里面的this.request 指的也是requestFacade的实例对象;
    我们要修改什么值 重写HttpServletRequestWrapper的对应方法即可

  3. 再次提醒 ,本文基于的是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);
    }
}

响应示例结果:

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟秋与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值