姊妹篇《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;
}
}
参考文章首部姊妹篇第13节
文章评论