后端请求响应加解密
- 前言
- 实现步骤
-
- [1. Filter将ServletRequest改造成可重复读](#1. Filter将ServletRequest改造成可重复读)
- [2. 使用拦截器对请求体进行解密](#2. 使用拦截器对请求体进行解密)
- [3. 使用ResponseBodyAdvice 进行响应加密](#3. 使用ResponseBodyAdvice 进行响应加密)
- 测试效果
- 附录
- 扩展知识
前言
由于安全要求,需要对请求响应做加解密。这边设计思路如下:
在前端存储一个默认的国密2密钥对,在用户请求未登录的接口时使用。登录之后重新生成一个密钥对,前端存储起来。前端请求的时候,使用公钥加密,后端使用私钥解密。后端响应的时候,生成国密4秘钥,使用国密4秘钥将响应内容加密,使用国密2公钥,将国密4秘钥加密,响应内容+时间戳,使用国密3做签名。形成如下数据结构
json
{
"data": "国密4加密的响应数据",
"t": 1763103461390,
"encryptedKey": "国密2公钥加密的sm4Key",
"sign": "国密3加密响应数据原文+时间搓形成的签名"
}
基本逻辑如上,但是有时候加解密会导致测试时十分不方便,或者一些特殊接口不要加解密。因为我定义了两个注解@NoReqDecrypt表示绕过请求数据解密,@NoRespEncrypt表示绕过响应数据加密。
这边主要关注后端加解密的逻辑,因此暂时抛开用户登录,生成秘钥,获取当前用户密钥对等的逻辑。
OK,那么如上所属,后端的基本逻辑已经确定,开始写吧。
实现步骤
1. Filter将ServletRequest改造成可重复读
默认情况,servletRequest请求中的getInputStream()被读取之后就无法再次读取了,因此需要使用装饰器模式进行包装以下,将流中的请求体缓存下来。
java
//
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();
}
}
}
java
@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将这个请求分发给哪个方法进行处理,因此无法判断。
java
// 请求解密拦截器
@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);
}
}
配置开启拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private Sm2EncryptInterceptor sm2EncryptInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sm2EncryptInterceptor)
// 拦截所有请求
.addPathPatterns("/**")
// 排除静态资源
.excludePathPatterns("/static/**");
}
}
3. 使用ResponseBodyAdvice 进行响应加密
对于响应加密其实有几个选择
- 在Filter的
chain.doFilter()之后进行加密 - 在
ResponseBodyAdvice进行加密
当然ResponseBodyAdvice是一个最合适也最优雅的方式。
java
@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
<?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>
注解类
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoReqDecrypt {
}
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRespEncrypt {
}
加解密工具类
java
public class Sm2Constant {
public static final String DEFAULT_PUBLIC_KEY = "044a034dd21fab64a667696e164e15c6fa501f07c071b02822d9c8b7c6077c710d28950be906fc344f50a07d340b7b9b29652ace5a5bdb8afdd28f3a8ed952007e";
public static final String DEFAULT_PRIVATE_KEY = "74b9376b9e0c72ce4de159193b4d9161486c1b009c589d23f13432a7a606582e";
}
java
@Data
public class Sm2Key {
private String publicKeyHex;
private String privateKeyHex;
public Sm2Key(String publicKeyHex, String privateKeyHex) {
this.publicKeyHex = publicKeyHex;
this.privateKeyHex = privateKeyHex;
}
}
java
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);
}
}
扩展知识
完整执行流程
- Filter预处理阶段 :HTTP请求首先经过所有配置的Filter,执行doFilter方法中
filterChain.doFilter()之前的逻辑 - DispatcherServlet接收请求:请求到达DispatcherServlet,作为前端控制器统一捕获
- HandlerInterceptor.preHandle():在DispatcherServlet调用HandlerMapping解析到对应处理器后,执行拦截器链中所有拦截器的preHandle方法
- RequestBodyAdvice.beforeBodyRead() :在执行Handler方法前,对请求体进行处理。并且只有方法参数上添加这个注释
@RequestBody的才会执行 - Controller Handler执行:实际执行业务逻辑的处理器方法
- ResponseBodyAdvice.beforeBodyWrite():在Handler方法返回后、写入响应体前执行。
- HandlerInterceptor.postHandle():执行拦截器链中所有拦截器的postHandle方法
- Filter后处理阶段:执行doFilter方法中filterChain.doFilter()之后的逻辑
- HandlerInterceptor.afterCompletion():在DispatcherServlet请求处理的最后执行,无论是否抛出异常
踩坑
- HandlerInterceptor.postHandle() 无法进行响应的修改,可能导致ouputStream冲突导致响应为空
- HandlerInterceptor.preHandle() 是无法使用装饰器模式的,所以对请求的包装,要在Filter中完成。