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

前言

由于安全要求,需要对请求响应做加解密。这边设计思路如下:
在前端存储一个默认的国密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());
}
@Override
public ServletInputStream getInputStream() {
return new CachedServletInputStream(this.cachedBody);
}
@Override
public 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);
}
@Override
public boolean isFinished() {
return input.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return input.read();
}
}
}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RepeatableFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public 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);
}
@Override
public 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();
@Override
public 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 {
@Resource
private Sm2EncryptInterceptor sm2EncryptInterceptor;
@Override
public 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();
  @Override
  public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    return needEncrypt(returnType);
    }
    @Override
    public 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中完成。
posted @ 2025-12-10 17:59  clnchanpin  阅读(0)  评论(0)    收藏  举报