news 2026/4/15 9:49:05

自定义全局零侵入加密注解@EncryptResponse,解密注解@DecryptRequest。可集成到任意服务或者项目,复制即用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自定义全局零侵入加密注解@EncryptResponse,解密注解@DecryptRequest。可集成到任意服务或者项目,复制即用

(本文加解密使用规则即为博主之前写的加解密文章) 地址: SM4加解密

本文介绍了基于SM4加解密的零侵入式API安全方案,主要包含两部分实现:

1、响应加密通过@EncryptResponse注解标记需要加密的接口,由EncryptResponseInterceptor拦截器对返回结果自动进行SM4加密处理;

2、请求解密通过@DecryptRequest注解标记需要解密的接口,由DecryptRequestInterceptor拦截器配合RequestWrapperFilter过滤器实现POST/PUT/GET请求的参数自动解密。

3、方案采用ThreadLocal存储解密数据,支持明密文混合传输,提供完整的参数解析器实现,并内置内存泄漏防护机制。

4、所有加解密操作通过Feign调用远程服务完成,开发者只需添加注解即可实现接口级安全防护,无需修改业务代码。

一、自定义加密注解相关代码及效果展示

1、自定义注解@DecryptRequest

博主这里是若依微服务项目,为了方便使用,故而放在com.ruoyi.common.security 目录下

package com.ruoyi.common.security.annotation; import java.lang.annotation.*; /** * 解密标记注解:仅需加在需要解密的接口上,原代码无需任何修改,零侵入。进去参数绑定前会自动解密并成参数绑定 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DecryptRequest { }

2、零侵入响应结果加密拦截器

结果返回给前端前自动完成加密处理,仅对标记上该注解接口生效

这里使用的加密依赖为本博主前面文章SM4加解密所写接口(这里是微服务feign api形式调用),各位可直接连同那边代码一起搬运使用,也可自己编写一套加解密规则进行替换使用。不影响整个注解效果

package com.ruoyi.common.security.interceptor; import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.security.annotation.EncryptResponse; import com.ruoyi.system.api.RemoteSMService; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; 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.stereotype.Component; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.lang.reflect.Method; /** * 响应结果加密拦截器 * 拦截标记@EncryptResponse注解的方法,对返回结果进行加密处理 */ @Component @ControllerAdvice public class EncryptResponseInterceptor implements ResponseBodyAdvice<Object> { // 从 Spring 容器中注入依赖: 加解密专用服务 feign api调用 @Autowired private RemoteSMService remoteSMService; /** * 判断是否需要执行加密处理 */ @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // 检查方法是否标记了EncryptResponse注解 Method method = returnType.getMethod(); boolean annotationPresent = method.isAnnotationPresent(EncryptResponse.class); // 2. 获取目标类(绕开代理) Class<?> targetClass = returnType.getDeclaringClass(); // 3. 获取目标类的真实方法(AopUtils确保拿到原方法) Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); // 4. 用AnnotationUtils获取注解(即使被代理也能拿到) EncryptResponse annotation = AnnotationUtils.findAnnotation(targetMethod, EncryptResponse.class); return method != null && annotationPresent; } /** * 对响应结果进行加密处理 */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { JSONObject object = JSONObject.from(body); // 非200状态码不加密 if(object.getIntValue("code") != 200){ return body; } // 获取注解信息 EncryptResponse annotation = returnType.getMethodAnnotation(EncryptResponse.class); if (annotation == null) { return body; } try { // 转换为JSON字符串作为加密入参 String jsonData = JSONObject.toJSONString(body); // 调用SM4加密接口 AjaxResult encryptResult = remoteSMService.sm4EncryptStr(jsonData, null); if (!encryptResult.isSuccess()) { return AjaxResult.error("加密失败:" + encryptResult.get("msg")); } String encryptedData = encryptResult.get("data").toString(); // 构建最终响应结果 AjaxResult finalResult = new AjaxResult(); finalResult.put("code", object.get("code")); finalResult.put("msg", object.get("msg")); finalResult.put("encrypted", encryptedData); return finalResult; } catch (Exception e) { // 加密过程异常不影响原始响应,仅打日志 e.printStackTrace(); return body; } } }

3、实际作用示例

1、未加密处理返回结果:原文代码及返回结果展示

1、请求信息

2、接口接参

3、未加密返回

2、加密处理返回结果:原文代码及结果展示

1、请求信息

(这里包含了下面的要讲解的前端加密传参请求数据)

2、未返回前明文信息

3、加密代码生效过程截图

4、实际返回结果信息(已加密)

3、作用及说明

1、自定义注解@EncryptResponse :仅实现对方法进行标记 不影响其他注解效果

2、拦截器EncryptResponseInterceptor :实现在接口处理完成返回给前端前进行拦截处理,并进行条件过滤,仅针对被标记后的方法及 code = 200请求响应进行处理,其他接口正常明文放行

3、使用示例

二、解密相关代码及效果展示

1、解密注解@DecryptRequest

博主这里是若依微服务项目,为了方便使用,故而放在com.ruoyi.common.security 目录下

package com.ruoyi.common.security.annotation; import java.lang.annotation.*; /** * 接口返回结果加密注解 * 标记此注解的方法会对返回结果进行SM4加密处理 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface EncryptResponse { /** * 是否加密整个响应体 * 默认为true,加密整个返回对象;false时可指定加密字段 */ boolean encryptAll() default true; /** * 需要加密的字段名(当encryptAll为false时生效) */ String[] encryptFields() default {}; }

2、POST/PUT请求体包装类:缓存流,支持重复读取

package com.ruoyi.common.security.config; import jakarta.servlet.ReadListener; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.StandardCharsets; /** * POST/PUT请求体包装类:缓存流,支持重复读取 */ public class BufferedServletRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public BufferedServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取原始流并缓存 try (InputStream is = request.getInputStream()) { body = is.readAllBytes(); } } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public boolean isFinished() { return bis.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) {} @Override public int read() throws IOException { return bis.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } // 获取缓存的请求体字符串 public String getBody() { return new String(body, StandardCharsets.UTF_8); } }

3、RequestWrapperFilter过滤器

package com.ruoyi.common.security.filter; import com.ruoyi.common.security.config.BufferedServletRequestWrapper; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; import java.io.IOException; // 新增过滤器(需注册到Spring) @Component public class RequestWrapperFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 仅包装POST/PUT请求 if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; String method = httpRequest.getMethod().toUpperCase(); if ("POST".equals(method) || "PUT".equals(method)) { request = new BufferedServletRequestWrapper(httpRequest); } } chain.doFilter(request, response); } }

4、零侵入拦截器:

package com.ruoyi.common.security.interceptor; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONValidator; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.common.security.annotation.DecryptRequest; import com.ruoyi.common.security.config.BufferedServletRequestWrapper; import com.ruoyi.common.security.utils.DecryptDataHolder; import com.ruoyi.system.api.RemoteSMService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; import java.util.*; /** * 零侵入拦截器: * 1. POST/PUT:解密encrypted字段,合并到原有JSON,存入ThreadLocal * 2. GET:解密encrypted字段,合并到原有参数,存入ThreadLocal * 3. 不修改原Request,不影响原有逻辑 */ @Component // 添加这个注解 @Slf4j public class DecryptRequestInterceptor implements HandlerInterceptor { @Autowired private RemoteSMService remoteSMService; // 构造方法日志,验证Spring是否创建实例 public DecryptRequestInterceptor() { log.info("=== DecryptRequestInterceptor 实例被创建 ==="); } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 仅处理Controller方法 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); // 2. 仅处理标记@DecryptRequest的接口 DecryptRequest decryptAnnotation = AnnotationUtils.findAnnotation(method, DecryptRequest.class); if (decryptAnnotation == null) { return true; } // 3. 按请求方式处理(POST/PUT/GET) String requestMethod = request.getMethod().toUpperCase(); switch (requestMethod) { case "POST": case "PUT": return handlePostPut(request, response); case "GET": return handleGet(request, response); default: return true; } } /** * POST/PUT处理:解密encrypted,合并到原有JSON */ private boolean handlePostPut(HttpServletRequest request, HttpServletResponse response) throws IOException { // 复用过滤器已包装的request,避免重复创建 BufferedServletRequestWrapper requestWrapper; if (request instanceof BufferedServletRequestWrapper) { requestWrapper = (BufferedServletRequestWrapper) request; } else { requestWrapper = new BufferedServletRequestWrapper(request); } String originalBody = requestWrapper.getBody(); // 校验请求体 if (!StringUtils.hasText(originalBody)) { writeError(response, "请求体为空"); return false; } // 解析原始JSON JSONObject originalJson; try { originalJson = JSONObject.parseObject(originalBody); } catch (Exception e) { writeError(response, "请求体不是合法JSON"); return false; } // 提取encrypted字段(无则直接放行,存入原始JSON) String encryptedStr = originalJson.getString("encrypted"); if (!StringUtils.hasText(encryptedStr)) { DecryptDataHolder.setDecryptedBody(originalBody); return true; } // 调用Feign解密 String decryptedData = decrypt(encryptedStr, request, response); if (decryptedData == null) { return false; } // 合并数据:原有JSON + 解密后的JSON(解密字段覆盖同名原始字段) JSONObject decryptedJson = JSONObject.parseObject(decryptedData); originalJson.remove("encrypted"); // 移除密文字段 originalJson.putAll(decryptedJson); // 合并新字段 String finalBody = originalJson.toJSONString(); // 存入ThreadLocal(供参数解析器读取) DecryptDataHolder.setDecryptedBody(finalBody); return true; } /** * GET处理:解密encrypted,合并到原有参数 */ private boolean handleGet(HttpServletRequest request, HttpServletResponse response) throws IOException { // 提取原始GET参数(Map<String, String[]>) Map<String, String[]> originalParams = new HashMap<>(request.getParameterMap()); // 提取encrypted字段(无则直接放行) if (!originalParams.containsKey("encrypted")) { return true; } String[] encryptedArr = originalParams.get("encrypted"); String encryptedStr = (encryptedArr != null && encryptedArr.length > 0) ? encryptedArr[0] : ""; if (!StringUtils.hasText(encryptedStr)) { return true; } // 调用Feign解密 String decryptedData = decrypt(encryptedStr, request, response); if (decryptedData == null) { return false; } // 合并数据:原有参数 + 解密后的参数(解密字段覆盖同名原始参数) JSONObject decryptedJson = JSONObject.parseObject(decryptedData); originalParams.remove("encrypted"); // 移除密文字段 // 转换为Map<String, String[]>格式,适配GET参数规范 decryptedJson.forEach((key, value) -> { if (value == null) { originalParams.put(key, new String[]{""}); } else if (value.getClass().isArray() || value instanceof Iterable) { // 处理数组/集合类型 Iterable<?> iterable; if (value.getClass().isArray()) { // 数组转List(实现Iterable) iterable = Arrays.asList((Object[]) value); } else { // 集合直接强转 iterable = (Iterable<?>) value; } // 转为String数组 List<String> strList = new ArrayList<>(); iterable.forEach(item -> strList.add(item == null ? "" : item.toString())); String[] strArr = strList.toArray(new String[0]); originalParams.put(key, strArr); } else { // 普通类型(数字、布尔等)转为字符串数组 originalParams.put(key, new String[]{value.toString()}); } }); // 存入ThreadLocal(供参数解析器读取) DecryptDataHolder.setDecryptedGetParams(originalParams); return true; } /** * 通用解密逻辑(调用Feign接口) */ private String decrypt(String encryptedStr, HttpServletRequest request, HttpServletResponse response) throws IOException { // 校验appid(从请求头获取,可根据业务调整) String appid = request.getHeader("appid"); if (StringUtils.isEmpty(appid)){ appid = null; } // 调用Feign解密接口 AjaxResult decryptResult = remoteSMService.sm4DecryptStr(encryptedStr, appid); if (!decryptResult.isSuccess()) { writeError(response, "解密失败:" + decryptResult.get("msg")); return null; } String decryptedData = decryptResult.get("data").toString(); if (!JSONValidator.from(decryptedData).validate()) { writeError(response, "解密后数据不是合法JSON"); return null; } return decryptResult.get("data").toString(); } /** * 输出错误响应 */ private void writeError(HttpServletResponse response, String msg) throws IOException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSONObject.toJSONString(AjaxResult.error(msg))); response.getWriter().flush(); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("清理ThreadLocal"); // 清理ThreadLocal,避免内存泄漏 DecryptDataHolder.clear(); } }

5、零侵入参数解析器:Get/POST的非RequestBody 请求参数

6、零侵入解析器: POST/PUT 请求的@RequestBody参数则通过DecryptRequestBodyAdvice增强

7、存储解密后的请求体(POST/PUT),供拦截器和参数解析器共享

package com.ruoyi.common.security.utils; import java.util.Map; /** * 存储解密后的请求体(POST/PUT),供拦截器和参数解析器共享 */ public class DecryptDataHolder { // 存储POST/PUT解密后的完整JSON字符串 private static final ThreadLocal<String> DECRYPTED_BODY = new ThreadLocal<>(); // 存储GET解密后的完整参数Map(String[]适配多值参数) private static final ThreadLocal<Map<String, String[]>> DECRYPTED_GET_PARAMS = new ThreadLocal<>(); // POST/PUT相关 public static void setDecryptedBody(String body) { DECRYPTED_BODY.set(body); } public static String getDecryptedBody() { return DECRYPTED_BODY.get(); } // GET相关 public static void setDecryptedGetParams(Map<String, String[]> params) { DECRYPTED_GET_PARAMS.set(params); } public static Map<String, String[]> getDecryptedGetParams() { return DECRYPTED_GET_PARAMS.get(); } // 清理资源,避免内存泄漏 public static void clear() { DECRYPTED_BODY.remove(); DECRYPTED_GET_PARAMS.remove(); } }

8、WebMvcConfig拦截器配置修改

package com.ruoyi.common.security.config; import com.ruoyi.common.security.interceptor.DecryptRequestInterceptor; import com.ruoyi.common.security.interceptor.HeaderInterceptor; import com.ruoyi.common.security.resolver.DecryptedArgumentResolver; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; /** * 拦截器配置 * * @author ruoyi */ @Configuration @Slf4j public class WebMvcConfig implements WebMvcConfigurer { /** * 不需要拦截地址 */ public static final String[] excludeUrls = {"/login", "/logout", "/refresh", "/static/**", "/favicon.ico"}; // 从 Spring 容器中注入拦截器,而不是手动 new @Autowired @Lazy private DecryptRequestInterceptor decryptRequestInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(getHeaderInterceptor()) .addPathPatterns("/**") .excludePathPatterns(excludeUrls) .order(-10); // 打印日志,验证是否执行了注册逻辑 log.info("=== 开始注册DecryptRequestInterceptor ==="); registry.addInterceptor(decryptRequestInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludeUrls) .order(-1); log.info("=== DecryptRequestInterceptor 注册完成 ==="); } /** * 自定义请求头拦截器 */ public HeaderInterceptor getHeaderInterceptor() { return new HeaderInterceptor(); } // 新增:注册自定义参数解析器 @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { // 将自定义解析器添加到列表中 resolvers.add(0, new DecryptedArgumentResolver()); } }

9、实际使用示范及效果截图

1、使用方式示例

(在需要解密的接口上方打上注解即可)

2、Post请求示例

1、纯明文请求
1、请求参数

2、接收接收参数

3、返回结果

2、纯加密请求
1、请求参数

2、拦截器效果展示

3、接口实际接收参数显示

4、返回结果

5、数据库展示

6、入参说明

{

"encrypted":"m5Z8vwV7JTE0l1qQ5Ha7+/IMnjdVTU4vWTqsgYXby77m1yXVsAF1k3ECpfjtHV119vePPtZdVuf/ib6wU8kj6uGgdS7f8dIr0f8mscHjs+qS2/n0iSTYjk/m5oMnDk89"

}

这里是后端直接生成的加密数据,前端相同规则的话,生成出来的密文和后端生成的密文相同

红色参数部分明文为:

3、部分明文部分加密
1、请求参数截图

encrypted 参数说明,示例如下图所示,红色框框区域则为被加密的明文json字符串数据

2、明密混搭请求数据结构

3、拦截器解密后组装后数据

4、实际接口接收到参数(已对帐号密码解密完成并绑定赋值给对象)

5、请求结果

3、Get请求示例

1、无任何加密数据传参
1、请求参数

2、接口接收参数

2、纯加密请求
1、请求参数

2、接口接收参数

3、部分明文部分加密
1、请求参数

2、接口接收参数

4、前端加密说明

1、前端所有加密传递的信息key为encryptedvalue:加密后密文

2、待加密明文必须为json字符串形式,不能是其他格式

3、返回加密后结果,data中的字符串即为加密后密文

4、具体前端加密示例可参照本博主另一篇SM4加解密文章中的前端示例源码中的加解密方式
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/4 1:49:51

【学习心得】Python好库推荐——pyttsx3

pyttsx3&#xff08;Python Text-to-Speech eXtended version 3&#xff09;是一个跨平台的 Python 库&#xff0c;用于将文本转换为语音&#xff08;Text-to-Speech, TTS&#xff09;。它可以在不依赖互联网连接的情况下&#xff0c;在本地将文本朗读出来&#xff0c;支持 Win…

作者头像 李华
网站建设 2026/4/11 1:56:33

Linux 通用软件包 AppImage 打包详解

格式介绍 - AppImageAppImage 是 Linux 系统中一种新型的软件包格式&#xff0c;它与 rpm、deb 这些软件包格式相比最大的不同便是&#xff1a;&#xff08;1&#xff09;无需安装&#xff0c;即用即删。&#xff08;2&#xff09;只需打包一次&#xff0c;便可到处运行。完美的…

作者头像 李华
网站建设 2026/4/14 20:48:32

软件测试工具选型全景指南:从需求对齐到落地实践

为什么工具选型关乎测试成败 在快速迭代的软件开发周期中&#xff0c;测试工具已从辅助手段演进为质量保障的核心基础设施。据统计&#xff0c;超过67%的测试团队曾因工具选型不当导致项目延期或质量漏洞。2025年测试工具生态呈现两大趋势&#xff1a;AI驱动的智能测试平台快速…

作者头像 李华
网站建设 2026/4/12 21:54:12

自动化测试投资回报率(ROI)分析与实践指南

在软件开发周期不断缩短的当下&#xff0c;自动化测试已成为保障产品质量、提升测试效率的关键手段。然而&#xff0c;许多测试团队在推行自动化测试时面临共同困惑&#xff1a;如何量化自动化测试的投入产出比&#xff1f;本文将从测试从业者视角&#xff0c;深入解析自动化测…

作者头像 李华
网站建设 2026/4/12 0:49:20

企业微信群消息定时发送竟然这么简单?三步搞定让效率翻倍!

你是不是还在手动发送每天的晨会提醒&#xff1f;或者每到下班时间就急着往群里发日报&#xff1f;别折腾了&#xff0c;现在有个方法能让你彻底解放双手。想想看&#xff0c;每天固定要发的通知、报表、提醒&#xff0c;如果都能自动完成&#xff0c;那该多省心啊。连趣云控制…

作者头像 李华