当前位置: 首页 > news >正文

【Java】使用国密2,3,4.仿照https 统一请求响应加解密

后端请求响应加解密

  • 前言
  • 实现步骤
    • 1. Filter将ServletRequest改造成可重复读
    • 2. 使用拦截器对请求体进行解密
    • 3. 使用ResponseBodyAdvice 进行响应加密
  • 测试效果
  • 附录
  • 扩展知识
    • 完整执行流程
    • 踩坑

前言

由于安全要求,需要对请求响应做加解密。这边设计思路如下:
在前端存储一个默认的国密2密钥对,在用户请求未登录的接口时使用。登录之后重新生成一个密钥对,前端存储起来。前端请求的时候,使用公钥加密,后端使用私钥解密。后端响应的时候,生成国密4秘钥,使用国密4秘钥将响应内容加密,使用国密2公钥,将国密4秘钥加密,响应内容+时间戳,使用国密3做签名。形成如下数据结构

{"data": "国密4加密的响应数据","t": 1763103461390,"encryptedKey": "国密2公钥加密的sm4Key","sign": "国密3加密响应数据原文+时间搓形成的签名"
}

基本逻辑如上,但是有时候加解密会导致测试时十分不方便,或者一些特殊接口不要加解密。因为我定义了两个注解@NoReqDecrypt表示绕过请求数据解密,@NoRespEncrypt表示绕过响应数据加密。
这边主要关注后端加解密的逻辑,因此暂时抛开用户登录,生成秘钥,获取当前用户密钥对等的逻辑。
OK,那么如上所属,后端的基本逻辑已经确定,开始写吧。

实现步骤

1. Filter将ServletRequest改造成可重复读

默认情况,servletRequest请求中的getInputStream()被读取之后就无法再次读取了,因此需要使用装饰器模式进行包装以下,将流中的请求体缓存下来。

// 
public class RepeatableRequestWrapper extends HttpServletRequestWrapper {private byte[] cachedBody;public RepeatableRequestWrapper(HttpServletRequest request) throws IOException {super(request);this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());}@Overridepublic ServletInputStream getInputStream() {return new CachedServletInputStream(this.cachedBody);}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream()));}public byte[] getCachedBody() {return cachedBody;}public void updateBody(String body){cachedBody = body.getBytes(StandardCharsets.UTF_8);}public void updateBody(byte[] body){cachedBody = body;}private static class CachedServletInputStream extends ServletInputStream {private final ByteArrayInputStream input;public CachedServletInputStream(byte[] buf) {this.input = new ByteArrayInputStream(buf);}@Overridepublic boolean isFinished() {return input.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() {return input.read();}}
}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RepeatableFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;// 对JSON请求进行包装ServletRequest requestWrapper = new RepeatableRequestWrapper(httpRequest);chain.doFilter(requestWrapper, servletResponse);}@Overridepublic void destroy() {Filter.super.destroy();}
}

2. 使用拦截器对请求体进行解密

这里需要说明一下,为什么不直接在Filter中直接解密,而要在拦截器中进行解密。
这是因为前面提的注解的小需求:@NoReqDecrypt,我需要判断处理的方法上是否有这个注解,默认解密,有则不解密。
但是Filter的执行顺序是优于DispatcherServlet,也就是说在Filter中chain.doFilter()之前,还不知道Spring将这个请求分发给哪个方法进行处理,因此无法判断。

// 请求解密拦截器
@Component
public class DecryptInterceptor implements HandlerInterceptor {private final ObjectMapper objectMapper = new ObjectMapper();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {RepeatableRequestWrapper requestWrapper = null;if (request instanceof RepeatableRequestWrapper){requestWrapper = (RepeatableRequestWrapper) request;}boolean needDecrypt = true;if (handler instanceof HandlerMethod handlerMethod) {System.out.println("请求--拦截器开始判断===================");// 检查是否需要解密NoReqDecrypt noReqDecrypt = handlerMethod.getMethodAnnotation(NoReqDecrypt.class);if (noReqDecrypt != null) {needDecrypt = false;}}if (needDecrypt && isJsonRequest(request) && requestWrapper != null) {try {String requestBody = new String(requestWrapper.getCachedBody());String decryptedData = decryptedData(requestBody);requestWrapper.updateBody(decryptedData);} catch(Exception e){throw new ServletException("SM2解密失败", e);}}return true;}private String decryptedData(String requestBody) throws JsonProcessingException {if (StringUtils.hasText(requestBody)) {ObjectNode jsonNodes = objectMapper.readValue(requestBody, ObjectNode.class);String encryptedData = jsonNodes.get("data").asText();if (StringUtils.hasText(encryptedData)) {return Sm2Util.decryptSm2(encryptedData, Sm2Constant.DEFAULT_PRIVATE_KEY);}else {return "";}}return "";}private boolean isJsonRequest(HttpServletRequest request) {String contentType = request.getContentType();return contentType != null && contentType.contains(MediaType.APPLICATION_JSON_VALUE);}
}

配置开启拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {@Resourceprivate Sm2EncryptInterceptor sm2EncryptInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(sm2EncryptInterceptor)// 拦截所有请求.addPathPatterns("/**")// 排除静态资源.excludePathPatterns("/static/**");}
}

3. 使用ResponseBodyAdvice 进行响应加密

对于响应加密其实有几个选择

  1. 在Filter的 chain.doFilter()之后进行加密
  2. ResponseBodyAdvice进行加密
    当然ResponseBodyAdvice是一个最合适也最优雅的方式。
@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return needEncrypt(returnType);}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {Map<String, Object> resMap = new HashMap<>(16);try {{System.out.println("ResponseBodyAdvice 加密开始============");String json = OBJECT_MAPPER.writeValueAsString(body);resMap = createEncryptMap(json);}} catch (JsonProcessingException e) {e.printStackTrace();}if (!ObjectUtils.isEmpty(resMap)){return resMap;}return body;}// 判断是否需要加密private boolean needEncrypt(MethodParameter returnType){return !returnType.hasMethodAnnotation(NoRespEncrypt.class);}// 加密响应体// 1.生成sm4Key// 2.使用sm4Key对json进行国密4加密// 3.使用公钥对sm4Key进行国密2加密// 4.生成时间戳// 5.json+时间戳进行国密3签名private Map<String,Object> createEncryptMap(String json){Map<String, Object> resMap = new HashMap<>(16);if (StringUtils.hasText(json)){String sm4Key = Sm2Util.generateSm4Key();String encryptedSm4Key = Sm2Util.encryptSm2(sm4Key, Sm2Constant.DEFAULT_PUBLIC_KEY);String encryptedResponse = Sm2Util.encryptSm4(json, sm4Key);long timestamp = System.currentTimeMillis();String sign = Sm2Util.signSm3(json + timestamp);resMap.put("data", encryptedResponse);resMap.put("encryptedKey",encryptedSm4Key);resMap.put("t",timestamp);resMap.put("sign",sign);}return resMap;}
}

至此整体加解密逻辑完成。后面附上使用到的工具类,注解,依赖等。

测试效果

在这里插入图片描述

附录

这边加解密工具,使用的是hutool的。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.zxh.test</groupId><artifactId>03_httpSm234</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.8</version></parent><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--国密--><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15to18</artifactId><version>1.69</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.39</version></dependency><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot3-starter</artifactId><version>1.44.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build>
</project>

注解类

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoReqDecrypt {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRespEncrypt {
}

加解密工具类

public class Sm2Constant {public static final String DEFAULT_PUBLIC_KEY = "044a034dd21fab64a667696e164e15c6fa501f07c071b02822d9c8b7c6077c710d28950be906fc344f50a07d340b7b9b29652ace5a5bdb8afdd28f3a8ed952007e";public static final String DEFAULT_PRIVATE_KEY = "74b9376b9e0c72ce4de159193b4d9161486c1b009c589d23f13432a7a606582e";
}
@Data
public class Sm2Key {private String publicKeyHex;private String privateKeyHex;public Sm2Key(String publicKeyHex, String privateKeyHex) {this.publicKeyHex = publicKeyHex;this.privateKeyHex = privateKeyHex;}
}
public class Sm2Util {/*** 生成一对 C1C2C3 格式的SM2密钥** @return 处理结果*/public static Sm2Key generateSm2Key() {//创建sm2 对象SM2 sm2 = SmUtil.sm2();byte[] privateKeyByte = BCUtil.encodeECPrivateKey(sm2.getPrivateKey());//这里公钥不压缩  公钥的第一个字节用于表示是否压缩  可以不要byte[] publicKeyByte = ((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false);String privateKey = HexUtil.encodeHexStr(privateKeyByte);String publicKey = HexUtil.encodeHexStr(publicKeyByte);return new Sm2Key(publicKey, privateKey);}/*** 获取SM2加密工具对象** @param privateKey 加密私钥* @param publicKey  加密公钥* @return 处理结果*/private static SM2 getSm2(String privateKey, String publicKey) {ECPrivateKeyParameters ecPrivateKeyParameters = null;ECPublicKeyParameters ecPublicKeyParameters = null;if (StrUtil.isNotBlank(privateKey)) {ecPrivateKeyParameters = BCUtil.toSm2Params(privateKey);}if (StrUtil.isNotBlank(publicKey)) {if (publicKey.length() == 130) {//这里需要去掉开始第一个字节 第一个字节表示标记publicKey = publicKey.substring(2);}String xhex = publicKey.substring(0, 64);String yhex = publicKey.substring(64, 128);ecPublicKeyParameters = BCUtil.toSm2Params(xhex, yhex);}//创建sm2 对象SM2 sm2 = new SM2(ecPrivateKeyParameters, ecPublicKeyParameters);sm2.usePlainEncoding();sm2.setMode(SM2Engine.Mode.C1C2C3);return sm2;}/*** SM2加密** @param  data : 需要加密的数据* @param  publicKey : 公钥* @return 加密结果*/public static String encryptSm2(String data, String publicKey) {//创建sm2 对象SM2 sm2 = getSm2(null, publicKey);return sm2.encryptBcd(data, KeyType.PublicKey);}/*** SM2解密** @param  dataHex : 需要加密的数据* @param  privateKey : 私钥* @return 解密结果*/public static String decryptSm2(String dataHex, String privateKey) {//创建sm2 对象SM2 sm2 = getSm2(privateKey, null);return StrUtil.utf8Str(sm2.decryptFromBcd(dataHex, KeyType.PrivateKey));}/*** 摘要加密算法SM3* @param aaa aaa* @return sign*/public static String signSm3(String aaa){return SmUtil.sm3(aaa);}/*** 生成国密4秘钥* @return key*/public static String generateSm4Key(){SM4 sm4 = SmUtil.sm4();return HexUtil.encodeHexStr(sm4.getSecretKey().getEncoded());}/*** SM4加密* @param content 明文内容* @param sm4Key 16进制格式的密钥* @return 加密后的16进制字符串*/public static String encryptSm4(String content, String sm4Key) {if (StrUtil.isBlank(content)) {return "";}SM4 sm4 = new SM4(HexUtil.decodeHex(sm4Key));return sm4.encryptHex(content);}/*** SM4解密* @param content 密文内容* @param sm4Key 16进制格式的密钥* @return 解密后的明文字符串*/public static String decryptSm4(String content, String sm4Key) {if (StrUtil.isBlank(content)) {return "";}SM4 sm4 = new SM4(HexUtil.decodeHex(sm4Key));return sm4.decryptStr(content);}
}

扩展知识

完整执行流程

  1. Filter预处理阶段‌:HTTP请求首先经过所有配置的Filter,执行doFilter方法中filterChain.doFilter()之前的逻辑
  2. DispatcherServlet接收请求‌:请求到达DispatcherServlet,作为前端控制器统一捕获
  3. HandlerInterceptor.preHandle():在DispatcherServlet调用HandlerMapping解析到对应处理器后,执行拦截器链中所有拦截器的preHandle方法
  4. RequestBodyAdvice.beforeBodyRead():在执行Handler方法前,对请求体进行处理。并且只有方法参数上添加这个注释@RequestBody的才会执行
  5. Controller Handler执行‌:实际执行业务逻辑的处理器方法
  6. ResponseBodyAdvice.beforeBodyWrite():在Handler方法返回后、写入响应体前执行。
  7. HandlerInterceptor.postHandle():执行拦截器链中所有拦截器的postHandle方法
  8. Filter后处理阶段‌:执行doFilter方法中filterChain.doFilter()之后的逻辑
  9. HandlerInterceptor.afterCompletion():在DispatcherServlet请求处理的最后执行,无论是否抛出异常

踩坑

  1. HandlerInterceptor.postHandle() 无法进行响应的修改,可能导致ouputStream冲突导致响应为空
  2. HandlerInterceptor.preHandle() 是无法使用装饰器模式的,所以对请求的包装,要在Filter中完成。
http://www.dtcms.com/a/609211.html

相关文章:

  • 华为对象存储:nginx代理临时访问地址后访问报错:Authentication Failed
  • 【2025-11-13】软件供应链安全日报:最新漏洞预警与投毒预警情报汇总
  • 【玩转多核异构】T153核心板RISC-V核的实时性应用解析
  • 单周期Risc-V指令拆分与datapath绘制
  • Java+EasyExcel 打造学习平台视频学习时长统计系统
  • 【PHP】使用buildsql构造子查询
  • 防火墙主要有哪些类型?如何保护网络安全?
  • 在线商城网站制作如东住房和城乡建设局网站
  • Java 与 PHP 开发核心良好习惯笔记(含通用+语言特有)
  • AI 电影制作迈入新阶段:谷歌云Veo 3.1模型发布,实现音频全覆盖与精细化创意剪辑
  • C++函数式策略模式中配置修改
  • [MCP][]快速入门MCP开发
  • 为食堂写个网站建设免费毕业设计的网站建设
  • 云原生数据平台(cloudeon)--核心服务组件扩展
  • 字典或者列表常用方法介绍
  • 计算机网络中的地址体系全解析(包含 A/B/C 类地址 + 私有地址 + CIDR)
  • SpringBoot教程(三十四)| SpringBoot集成本地缓存Caffeine
  • 专业摄影网站推荐专业做卖菜的网站
  • Hadess V1.2.5版本发布,新增推送规则、制品扫描等,有效保障制品质量与安全
  • 华清远见25072班单片机高级学习day1
  • Apache Flink运行环境搭建
  • Node.js(v16.13.2版本)安装及环境配置教程
  • Flutter 每日库: device_info_plus获取设备详细信息
  • 小马网站建设网站备案好
  • 做某网站的设计与实现网页设计代码案例
  • 生产级 Rust Web 应用架构:使用 Axum 实现模块化设计与健壮的错误处理
  • 大模型三阶段训练:预训练、SFT、RLHF解决的核心问题
  • 记/基准] RELIABLE AND DIVERSE EVALUATION OF LLM MEDICAL KNOWLEDGE MASTERY
  • TensorFlow深度学习实战(9)——卷积神经网络应用
  • LeetCode 分类刷题:203. 移除链表元素