【springboot】springboot接口参数全局解密,解决request内容修改后如何重新设置回去的问题
文章目录
- 核心思路
- spring&servelt基础
- 核心接口类
- 核心代码
- body解密核心原理讲解
- get解密核心原理讲解
- get query请求讲解
- get pathVariables请求讲解
- 总结
本文不仅介绍了body内容修改后如何传递,也介绍了get请求 在修改内容后如何继续传递。
【原创作者 csdn: 孟秋与你】
核心思路
- 拦截每次请求 所以要么在拦截器 要么在过滤器中做 (正常来说 其实只能在过滤器做)
- 修改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 {@Overrideprotected 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; // 示例}@Overridepublic String getParameter(String name) {String[] paramArray = params.get(name);return paramArray != null && paramArray.length > 0 ? paramArray[0] : null;}@Overridepublic Map<String, String[]> getParameterMap() {return params;}@Overridepublic String[] getParameterValues(String name) {String[] parameterValues = super.getParameterValues(name);// 模拟get请求(query请求 即form表单提交)的解密return new String[]{"加密"};}@Overridepublic Enumeration<String> getAttributeNames() {return super.getAttributeNames();}@Overridepublic 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);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {String s = new String(body, StandardCharsets.UTF_8);System.out.println(s);return new CachedBodyServletInputStream(body);}@Overridepublic 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是没什么区别的 )
@Overridepublic 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类
相关文章:

【springboot】springboot接口参数全局解密,解决request内容修改后如何重新设置回去的问题
文章目录 核心思路spring&servelt基础核心接口类核心代码 body解密核心原理讲解get解密核心原理讲解get query请求讲解get pathVariables请求讲解 总结 本文不仅介绍了body内容修改后如何传递,也介绍了get请求 在修改内容后如何继续传递。 【原创作者 csdn: 孟秋…...
yml基本语法
YAML(YAML Ain’t Markup Language)是一种简洁且易读的数据序列化格式,常用于配置文件。Spring Boot 中的 application.yml 文件使用 YAML 来配置应用程序的属性。 YAML 基本语法 1. 键值对 基本的键值对表示形式为:key: value…...

橙色简洁大气体育直播自适应模板赛事直播门户自适应网站源码
源码名称:酷黑简洁大气体育直播自适应模板赛事直播门户网站 源码开发环境:帝国cms 7.5 安装环境:phpmysql 带采集,可以挂着电脑上自动采集发布,无需人工操作! 橙色简洁大气体育直播自适应模板赛事直播门户…...

【启明智显技术分享】工业级HMI芯片Model系列GUI合成到项目中的指南
在工业自动化、智能终端HMI、车载仪表盘等领域,高性能的HMI(人机界面)芯片是不可或缺的核心组件。启明智显推出的Model系列(如Model3C、Model3、Model4)HMI芯片,以其卓越的性能和广泛的应用领域,…...

开源服务器运维工具1Panel
1Panel是杭州飞致云信息科技有限公司推出的一款现代化、开源的Linux服务器运维管理面板。 以下是对1Panel的详细介绍: 一、基本信息 产品名称:1Panel所属公司:杭州飞致云信息科技有限公司编写语言:Golang上线时间:20…...

新版本源2.0大模型发布:Yuan2-2B-July-hf
引言 近日,浪潮信息的新一代基础语言大模型源2.0 迎来了重要更新。浪潮信息正式发布了 Yuan2-2B-July-hf 模型,标志着源2.0系列模型在性能和功能上的进一步提升。这一版本将为开发者和研究人员提供更强大的工具,以满足各种语言处理需求。…...

用python生成GIF动图—用于博客插图或封面等
生成GIF动图🚀 由于目前自己是在做大模型,还有一些树莓派硬件之类的东西,一是大模型的流式输出的例子需要用到GIF,二是做单片机的时候例如一些灯的闪烁和变化需要用到,所以之前也是一直有这个打算所以就记录一下这个生…...

[RCTF2019]draw
下载是一个文本文档,百度AI cs pu lt 90 fd 500 rt 90 pd fd 100 rt 90 repeat 18[fd 5 rt 10] lt 135 fd 50 lt 135 pu bk 100 pd setcolor pick [ red orange yellow green blue violet ] repeat 18[fd 5 rt 10] rt 90 fd 60 rt 90 bk 30 rt 90 fd 60 pu lt 90 f…...
设计模式 - 责任链模式
💝💝💝首先,欢迎各位来到我的博客!本文深入理解设计模式原理、应用技巧、强调实战操作,提供代码示例和解决方案,适合有一定编程基础并希望提升设计能力的开发者,帮助读者快速掌握并灵活运用设计模式。 💝💝💝如有需要请大家订阅我的专栏【设计模式】哟!我会定…...

jpg怎么转换成pdf?6个简单方法,实现jpg转换成pdf
你是否也曾想将jpg图片转换为pdf格式文档呢?亦或者在处理文档或制作报告时,不知道怎么才能更快地将多张图片整合成一个pdf文件呢?如果你正在寻找简单快速的方法,又有哪些工具可以帮助您完成图片转pdf呢?别着急…...
ptrade排坑笔记——使用量化交易的时候有报错提示!
前言 今天要和大家分享一个遇见的问题,有客户反馈,自己在使用量化交易的时候,会有报错!会在后文分享我们是如何解决这个问腿的! 一、问题描述 客户主要遇见的问题是,量化在进行交易的过程中,…...
C#-MemoryMarshal
MemoryMarshal 类是 .NET 中用于处理内存的工具类,它提供了一组静态方法,用于在托管代码中以安全和高效的方式操作内存块。MemoryMarshal 类主要用于处理原始内存数据而不需要进行复制,这对于性能关键的操作非常有用。 MemoryMarshal 类包含…...

Java并发编程的艺术
Java作为一门面向对象的编程语言,自1995年推出以来,一直以其稳定性、跨平台性和丰富的API受到广大开发者的喜爱。在Java的发展历程中,并发编程一直是其重要的特性之一。本文将探讨Java并发编程的艺术,解析其核心概念和常用并发工具…...
华为 OLT 添加 ONU 配置 (SNMP管理模式)
上网业务数据规划 OLT PON口 0/8/0 ONU_ID 0 ONU 序列号 4857544323BE233B 外层 VLAN ID 2012 内层VLAN ID 35 用户 FE 端口 ONU 0/1/1 用户VLAN 35 DBA带宽类型 Type 2 流量模板编号 10 DBA 模板编号 30 ONU线路模板编号 40 T-CONT (网管) 0 T-CONT(业务_ 2 GEM (网管) 0 …...
【JavaScript】[]和{} 的转换
背景 ([])? true:false ({})? true:false ([] true)? true:false ({} true)? true:false ([] true)? true:false ({} true)? true:false分析 [ ]和{ } 都是复杂类型,以上都是三目运算符判断 1.判断[ ]和{ } 是否存在 声明了这些已经分配了内存…...

C#关于多线程的线程问题
using System.Text; namespace 平时练习8._19day06 {internal class Program{static async Task Main(string[] args){Console.WriteLine(Thread.CurrentThread.ManagedThreadId );StringBuilder sb new StringBuilder();for (int i 0; i < 10000; i){sb.Append("…...

eclipse打开失败 java was started but returned exit code=13
报错详细信息如下 原因:eclipse版本和jdk版本不一致。系统之前jdk是1.6,然后安装1.8之后默认修改了环境变量。导致eclipse启动失败 解决方案:修改eclipse目录下的eclipse.ini文件增加一下内容。文档说明:eclipse.ini - Eclipsepe…...

【计算机网络】应用层自定义协议与序列化
记得在上一节我们说过TCP中的读取时需要改进,这节就可以解决读取问题了。 目录 应用层再谈 "协议"网络版计算机方案一方案二 序列化 和 反序列化 重新理解 read、write、recv、send 和 tcp 为什么支持全双工 应用层 再谈 “协议” 我们在UDP与TCP中写的…...

企业级无线局域网(WLAN)架构:高效部署策略与技术指南
前言:无线网络直接影响整体网络性能,在当今企业网环境中,已有超过一半的数据流量通过无线信道传输,随着物联网技术的普及,无线网将承载更多的关键业务流量。企业/园区场景的无线网络值得考虑的关键因素有很多ÿ…...

【Python-办公自动化】1秒筛选12个月指定逻辑数值
欢迎来到"花花 Show Python",一名热爱编程和分享知识的技术博主。在这里,我将与您一同探索Python的奥秘,分享编程技巧、项目实践和学习心得。无论您是编程新手还是资深开发者,都能在这里找到有价值的信息和灵感。 自我介绍: 我热衷于将复杂的技术概念以简单易懂…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

centos 7 部署awstats 网站访问检测
一、基础环境准备(两种安装方式都要做) bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats࿰…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...