(本文加解密使用规则即为博主之前写的加解密文章) 地址: 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字符串数据