稷然如此

  • 首页
  • 文章分类
    • AI
    • Android
    • Java
    • Shell
    • Vue
    • C#
    • Python
    • 数据库
    • 组件
    • 其他
    • Game
  • 常用命令
    • Docker
    • Git
    • Linux
  • 操作系统
    • CentOS
    • Ubuntu
    • Windows
    • Kylin
  • 工具
    • IntelliJ IDEA
    • Visual Studio Code
稷然如此
不积跬步,无以至千里
  1. 首页
  2. 文章分类
  3. Java
  4. 正文

Spring Boot SM2 加解密

2024年11月7日 553点热度 1人点赞

姊妹篇《Spring Cloud Gateway 网关 SM2 加解密》

相较于 Spring Cloud Gateway 网关加密,Spring Boot 就比较简单了。为什么 GetMapping、DeleteMapping 要用 OncePerRequestFilter,而PostMapping、PutMapping 用的 RequestBodyAdvice 可以参考《Spring MVC 常见拦截器的区别》或自行查阅相关资料,当然 PostMapping、PutMapping也可以写到 OncePerRequestFilter。来吧,直接进入正题:

1、引用工具包

引用 Hutool 和加密算法库 Bouncy Castle Crypto
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.22</version>
</dependency>

<dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcprov-jdk15to18</artifactId>
	<version>1.79</version>
</dependency>

2、创建sm2密钥对对象

package com.akim.cloud.framework.common.util.crypto;

import lombok.Data;

/**
 * @author akim
 * @version 1.0
 * @date 2023/3/29 14:10
 * @desc sm2密钥对
 */
@Data
public class SM2Key {
    /**
     * 服务端加密公钥,对应私钥由客户端持有(clientPrivateKey)
     */
    private String serverPublicKey;

    /**
     * 服务端解密私钥,对应公钥由客户端持有(clientPublicKey)
     */
    private String serverPrivateKey;

    /**
     * 客户端加密公钥,对应私钥由服务端持有(serverPrivateKey)
     */
    private String clientPublicKey;

    /**
     * 客户端解密私钥,对应公钥由服务端持有(serverPublicKey)
     */
    private String clientPrivateKey;
}

3、创建sm2工具类

package com.akim.cloud.framework.common.util.crypto;

import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.BCUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;

/**
 * @author akim
 * @version 1.0
 * @date 2023/3/29 14:10
 * @desc sm2工具类
 */
public class SM2Util {
    /**
     * 生成前后端加解密密钥对
     *
     * @return
     */
    public static SM2Key generate() {
        SM2Key sm2Key = new SM2Key();
        SM2 sm2 = SmUtil.sm2();
        // 设置服务端公钥
        sm2Key.setServerPublicKey(HexUtil.encodeHexStr(BCUtil.encodeECPublicKey(sm2.getPublicKey())));
        // 设置客户端私钥
        sm2Key.setClientPrivateKey(HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey())));
        sm2 = SmUtil.sm2();
        // 设置客户端公钥
        sm2Key.setClientPublicKey(HexUtil.encodeHexStr(BCUtil.encodeECPublicKey(sm2.getPublicKey())));
        // 设置客户端私钥
        sm2Key.setServerPrivateKey(HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey())));
        return sm2Key;
    }

    public static String encrypt(String publicKey, String data) {
        return SmUtil.sm2(null, publicKey)
                .encryptHex(data.getBytes(), KeyType.PublicKey)
                .substring(2); // 去掉04
    }

    public static String decrypt(String privateKey, String data) {
        return SmUtil.sm2(privateKey, null)
                .decryptStr(data.startsWith("04") ? data : "04" + data, KeyType.PrivateKey);
    }

//    public static void main(String[] args) {
//        try {
//            SM2Key sm2Key = SM2Util.generate();
//            String testStr = "我是测试内容";
//            // 服务端加密
//            String enStr = SM2Util.encrypt(sm2Key.getServerPublicKey(), testStr);
//            System.out.println("服务端加密结果:" + enStr);
//            // 客户端解密
//            String deStr = SM2Util.decrypt(sm2Key.getClientPrivateKey(), enStr);
//            System.out.println("客户端解密结果:" + deStr);
//
//            // 客户端加密
//            String enStr2 = SM2Util.encrypt(sm2Key.getClientPublicKey(), testStr);
//            System.out.println("客户端加密结果:" + enStr);
//            // 服务端解密
//            String deStr2 = SM2Util.decrypt(sm2Key.getServerPrivateKey(), enStr2);
//            System.out.println("服务端解密结果:" + deStr2);
//        } catch (Exception e) {
//            System.out.println(e.getMessage());
//        }
//    }
}

4、配置application.yaml

akim:
  # 是否启用SM2国密全报文加密
  secret:
    enabled: true
    # 免加密接口配置
    excluded:
      # 以“,”逗号分隔
      paths: /api/auth/login

5、创建配置

package com.akim.boot.starter.web.common.crypto.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.Collections;
import java.util.Set;

/**
 * @author akim
 * @version 1.0
 * @date 2024/10/28 11:59
 * @desc
 */
@ConfigurationProperties(prefix = "akim.crypto")
@Data
public class CryptoProperties {
    /**
     * 是否启用
     */
    private Boolean enable = false;

    /**
     * 免加密接口
     */
    private Set<String> ignoreUrls = Collections.emptySet();
}
package com.akim.boot.starter.web.common.crypto.config;

import com.akim.boot.starter.web.common.crypto.filter.RequestParamsFilter;
import com.akim.boot.starter.web.common.crypto.handler.RequestBodyAdviceHandler;
import com.akim.boot.starter.web.common.crypto.handler.ResponseBodyAdviceHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author akim
 * @version 1.0
 * @date 2024/10/28 11:58
 * @desc 加密配置
 * 后续可以通过配置文件增加加解密类型进行拓展
 */
@AutoConfiguration
@ConditionalOnProperty(prefix = "akim.crypto", value = "enable", havingValue = "true")
@EnableConfigurationProperties(CryptoProperties.class)
@Slf4j
public class CryptoConfiguration implements WebMvcConfigurer {
    /**
     * GetMapping、DeleteMapping 请求拦截器
     *
     * @param cryptoProperties   加解密配置文件
     * @param applicationContext 上下文
     * @return GetMapping、DeleteMapping 请求拦截过滤器
     */
    @Bean
    public RequestParamsFilter requestParamsFilter(CryptoProperties cryptoProperties, ApplicationContext applicationContext) {
        return new RequestParamsFilter(cryptoProperties, applicationContext);
    }

    /**
     * PostMapping、PutMapping 请求拦截器
     *
     * @param properties 加解密配置
     * @return PostMapping、PutMapping 请求拦截器
     */
    @Bean
    public RequestBodyAdviceHandler requestBodyAdviceHandler(CryptoProperties properties) {
        return new RequestBodyAdviceHandler(properties);
    }

    /**
     * 加解密响应拦截器
     *
     * @param properties 加解密配置
     * @return 加解密响应拦截器
     */
    @Bean
    public ResponseBodyAdviceHandler responseBodyAdviceHandler(CryptoProperties properties) {
        return new ResponseBodyAdviceHandler(properties);
    }
}

6、创建 PostMapping、PutMapping 请求入参解密过滤器

package com.akim.boot.starter.web.common.crypto.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.akim.boot.starter.web.common.crypto.config.CryptoProperties;
import com.akim.boot.starter.web.common.crypto.message.CryptoHttpInputMessage;
import com.akim.boot.starter.web.core.util.WebUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.IOException;
import java.lang.reflect.Type;

/**
 * @author akim
 * @version 1.0
 * @date 2024/10/31 14:28
 * @desc PostMapping、PutMapping 请求拦截器
 */
@ControllerAdvice
@AllArgsConstructor
@Slf4j
public class RequestBodyAdviceHandler implements RequestBodyAdvice {
    private final CryptoProperties cryptoProperties;

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return ObjectUtil.isNotNull(WebUtils.getRequest()) && !CollUtil.contains(cryptoProperties.getIgnoreUrls(), WebUtils.getRequest().getRequestURI());
    }

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return new CryptoHttpInputMessage(inputMessage, parameter);
    }

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

 

package com.akim.boot.starter.web.common.crypto.message;

import cn.hutool.core.io.IoUtil;
import com.akim.boot.starter.common.exception.enums.GlobalErrorCodeConstants;
import com.akim.boot.starter.common.exception.util.ServiceExceptionUtil;
import com.akim.boot.starter.common.util.crypto.SM2Util;
import com.akim.boot.starter.web.core.util.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

/**
 * @author akim
 * @version 1.0
 * @date 2024/10/31 15:49
 * @desc
 */
@Slf4j
public class CryptoHttpInputMessage implements HttpInputMessage {
    private HttpHeaders headers;

    private InputStream body;

    public CryptoHttpInputMessage(HttpInputMessage inputMessage, MethodParameter parameter) {
        try {
            this.headers = inputMessage.getHeaders();
            this.body = inputMessage.getBody();
            //只对post请求进行加密
            if (parameter.hasMethodAnnotation(PostMapping.class) || parameter.hasMethodAnnotation(PutMapping.class)) {
                String decrypt = SM2Util.decrypt(WebUtils.getLoginUserCryptoKey().getServerPrivateKey(), IoUtil.read(inputMessage.getBody(), StandardCharsets.UTF_8));
                this.body = new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8));
            }
        } catch (IOException e) {
            throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.CRYPTO_DECRYPT_ERROR);
        }
    }

    @Override
    public InputStream getBody() throws IOException {
        return body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return headers;
    }
}

 

7、创建 GetMapping、DeleteMapping 请求入参解密过滤器

package com.akim.boot.starter.web.common.crypto.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.akim.boot.starter.common.exception.enums.GlobalErrorCodeConstants;
import com.akim.boot.starter.common.exception.util.ServiceExceptionUtil;
import com.akim.boot.starter.common.util.crypto.SM2Util;
import com.akim.boot.starter.common.util.servlet.ServletUtils;
import com.akim.boot.starter.web.common.crypto.config.CryptoProperties;
import com.akim.boot.starter.web.core.util.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author akim
 * @version 1.0
 * @date 2024/11/7 9:10
 * @desc GetMapping、DeletingMapping 请求拦截器
 */
@AllArgsConstructor
@Slf4j
public class RequestParamsFilter extends OncePerRequestFilter {
    private final CryptoProperties cryptoProperties;

    private final ApplicationContext context;

    private final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!CollUtil.contains(cryptoProperties.getIgnoreUrls(), request.getRequestURI())) {
            Map<String, HandlerMapping> handlerMappings = context.getBeansOfType(HandlerMapping.class);
            for (HandlerMapping handlerMapping : handlerMappings.values()) {
                try {
                    HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
                    if (handlerExecutionChain != null
                        && handlerExecutionChain.getHandler() instanceof HandlerMethod handlerMethod
                        && (handlerMethod.hasMethodAnnotation(GetMapping.class) || handlerMethod.hasMethodAnnotation(DeleteMapping.class))) {
                        Map<String, String> queryString = ServletUtils.getParamMap(request);
                        if (CollectionUtil.isNotEmpty(queryString)) {
                            for (Map.Entry<String, String> params : queryString.entrySet()) {
                                if (!params.getKey().equals("p")) {
                                    continue;
                                }
                                Map<String, String> parameterMap = new HashMap<>();
                                String decrypt = SM2Util.decrypt(WebUtils.getLoginUserCryptoKey().getServerPrivateKey(), params.getValue());
                                Matcher matcher = QUERY_PARAM_PATTERN.matcher(decrypt);
                                while (matcher.find()) {
                                    String name = matcher.group(1);
                                    String eq = matcher.group(2);
                                    String value = matcher.group(3);
                                    parameterMap.put(name, value != null ? value : (StringUtils.hasLength(eq) ? "" : null));
                                }
                                // 修改请求参数
                                HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
                                    @Override
                                    public String getParameter(String name) {
                                        return parameterMap.get(name);
                                    }

                                    @Override
                                    public Enumeration<String> getParameterNames() {
                                        // 返回所有参数名称的修改版本
                                        return Collections.enumeration(parameterMap.keySet().stream().toList());
                                    }

                                    @Override
                                    public String[] getParameterValues(String name) {
                                        return new String[] {parameterMap.get(name)};
                                    }
                                };
                                chain.doFilter(wrappedRequest, response);
                                return;
                            }
                            break;
                        }
                    }
                } catch (Exception e) {
                    throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.CRYPTO_DECRYPT_ERROR);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

8、创建响应出参加密过滤器

package com.akim.boot.starter.web.common.crypto.handler;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.akim.boot.starter.common.pojo.CommonResult;
import com.akim.boot.starter.common.util.crypto.SM2Util;
import com.akim.boot.starter.common.util.json.JsonUtils;
import com.akim.boot.starter.web.common.crypto.config.CryptoProperties;
import com.akim.boot.starter.web.core.util.WebUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @author akim
 * @version 1.0
 * @date 2024/10/31 14:51
 * @desc 出参加密拦截器
 */
@ControllerAdvice
@AllArgsConstructor
public class ResponseBodyAdviceHandler implements ResponseBodyAdvice<Object> {
    private final CryptoProperties cryptoProperties;

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public boolean supports(MethodParameter methodParameter, Class converterType) {
        return ObjectUtil.isNotNull(WebUtils.getRequest()) && !CollUtil.contains(cryptoProperties.getIgnoreUrls(), WebUtils.getRequest().getRequestURI());
    }

    @Override
    @SuppressWarnings("NullableProblems") // 避免 IDEA 警告
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof CommonResult<?> result) {
            if (ObjectUtil.isNotNull(result)) {
                String resultJson = JsonUtils.toJsonString(result);
                return SM2Util.encrypt(WebUtils.getLoginUserCryptoKey().getServerPublicKey(), resultJson);
            }
        }
        return body;
    }
}

9、前端Vue2处理参考

参考文章首部姊妹篇第13节
标签: sm2 Spring Spring Boot 加解密
最后更新:2024年11月7日

Akim

犇 骉 Java、C#、Python、Go、Android、MiniProgram、Bootstrap、Vue2

点赞
< 上一篇
下一篇 >
文章目录
  • 1、引用工具包
  • 2、创建sm2密钥对对象
  • 3、创建sm2工具类
  • 4、配置application.yaml
  • 5、创建配置
  • 6、创建 PostMapping、PutMapping 请求入参解密过滤器
  • 7、创建 GetMapping、DeleteMapping 请求入参解密过滤器
  • 8、创建响应出参加密过滤器
  • 9、前端Vue2处理参考

Copyright © 2025 aianran.com All Rights Reserved.

免责申明 | 隐私政策 | 服务条款 | 关于我们

黔ICP备2023008200号-1

贵公网安备 52010202003594号