如何在Spring Boot项目中添加国密SM4加密支持?——基于过滤器的实现
如何在Spring Boot项目中添加国密SM4加密支持?——基于过滤器的实现
引言
在数字化时代,数据安全至关重要,尤其是在API交互过程中,确保传输数据的安全性是保护隐私和机密信息的关键。中国制定了国密标准(国家商用密码算法),其中SM4是对称分组密码算法,广泛应用于需要高安全性的场景中,如金融交易等,对于保障国家安全具有重要作用。
本文聚焦于如何在Spring Boot项目中通过过滤器集成SM4加密支持,为应用提供额外的安全层,确保请求和响应的数据在传输过程中的安全性。下面将介绍具体的实施方法。
集成SM4加密支持于Spring Boot项目中主要是为了满足等保(信息安全等级保护)测评要求。等保是中国为保障信息系统安全而制定的一系列标准和规范,其核心目的是确保信息系统的安全性达到国家规定的水平,从而有效保护国家安全、公共利益和社会稳定。
目录
一、准备工作
-
添加maven依赖,bcprov这个依赖根据大家自己的JDK版本选择,我这里是JDK17,更多版本可以在maven仓库查找https://mvnrepository.com/
-
bcprov-jdk15on
是为支持 JDK 1.5 到 JDK 1.8 而设计的。这里的 "on" 后缀通常表示“旧版本的新实现”,意味着它是一个向后兼容的库,可以运行在较老版本的 Java 平台上。 -
bcprov-jdk18on
是为 JDK 1.8 及以上版本设计的。这个版本的库可能包含了更新的特性、安全修复和改进,以利用更高版本 Java 中的新功能和性能增强。
-
<!--国密加密依赖-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
- 基础知识介绍 - SM4算法
SM4是中国国家商用密码标准算法之一,是一种分组对称密码算法。它的主要特点是数据块大小为128位,密钥长度也是128位,采用32轮非线性迭代结构。作为一种高效、安全的加密算法,SM4被广泛应用于各类信息安全场景中,如金融交易、电子政务等。
在实际应用中,SM4通常用于保护敏感信息的传输和存储。通过将明文转换成密文,即使数据在传输过程中被截获,攻击者也难以获取有效信息。因此,在满足等保测评要求方面,SM4扮演着至关重要的角色。
接下来,我们将基于以上准备工作,进一步创建一个Sm4Util
工具类,以便于在Spring Boot项目中方便地使用SM4算法进行数据加密和解密操作。
二、创建SM4工具类 Sm4Util
SM4算法的基本概念及工作原理简介
基本概念
SM4是中国国家商用密码标准算法之一,属于分组对称加密算法。它最初被设计用于无线局域网产品,并于2012年正式 成为国家标准GB/T 32907-2016。作为一种高效、安全的加密算法,SM4主要用于保护数据的机密性和完整性。
- 分组大小:SM4采用128位(即16字节)的数据块作为处理单元。
- 密钥长度:同样为128位(即16字节),与AES等现代加密算法保持一致。
- 轮数:通过32轮非线性迭代结构进行数据变换,确保了算法的安全性。
工作原理
SM4的工作流程主要由以下几个步骤组成:
- 密钥扩展:将初始的128位密钥扩展成一个包含32个32位字的扩展密钥数组。这个过程包括S盒变换、线性变换以及轮函数的应用,目的是生成一系列子密钥以供后续加密使用。
- 轮函数:每一轮都应用了一个复杂的轮函数,该函数包含了四个主要操作:
- 非线性变换(S盒):通过查找表的方式实现,提供良好的混淆效果。
- 线性变换:基于矩阵运算,增加输出的复杂度和不可预测性。
- 轮密钥加:将当前轮的子密钥与状态进行异或操作,增强安全性。
- 字节重排:调整字节顺序,进一步扰乱数据结构。
- 加密过程:输入的明文首先被分成四个32位的字,然后依次经过32轮轮函数的处理,最终得到对应的密文。
- 解密过程:解密本质上是加密的逆过程。区别在于使用的轮密钥顺序相反,但轮函数本身保持不变。
应用场景
由于其高效且安全的特点,SM4被广泛应用于各种需要高安全性保障的场景中,如:
- 金融领域:在银行交易系统中,用于保护客户敏感信息的安全传输。
- 电子政务:政府内部文档和通讯加密,确保信息安全不泄露。
- 物联网(IoT):智能设备之间的通信加密,防止数据被窃取或篡改。
总之,SM4作为一种重要的国密算法,在中国的信息安全体系中扮演着不可或缺的角色。理解和掌握其基本概念和工作原理,对于开发人员来说至关重要,特别是在涉及国家安全和个人隐私保护的项目中。
实现
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import java.util.Base64;
public class Sm4Util {
// 示例密钥,实际应用中应使用安全的方式生成和存储密钥
private static final String KEY = "0123456789abcdef";
// 示例向量,实际应用中应使用安全的方式生成和存储初始化向量
//需要确保KEY和IV保持16字节长度
private static final String IV = "fedcba987654321";
/**
* 使用 SM4 算法对明文进行加密。
*
* @param plainText 要加密的明文字符串
* @return 加密后的 Base64 编码字符串
* @throws Exception 如果加密过程中发生错误
*/
public static String encrypt(String plainText) throws Exception {
// 创建一个带填充的缓冲块密码器,使用 CBC 模式和 SM4 引擎
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()));
// 初始化密码器为加密模式,并设置密钥和初始化向量
cipher.init(true, new ParametersWithIV(new KeyParameter(KEY.getBytes()), IV.getBytes()));
// 将明文转换为字节数组
byte[] input = plainText.getBytes();
// 计算输出缓冲区的大小
byte[] output = new byte[cipher.getOutputSize(input.length)];
// 进行分步加密
int length1 = cipher.processBytes(input, 0, input.length, output, 0);
int length2 = cipher.doFinal(output, length1);
// 返回加密后的内容,Base64 编码以便于传输和存储
return Base64.getEncoder().encodeToString(output);
}
/**
* 使用 SM4 算法对密文进行解密。
*
* @param encryptedText 加密后的 Base64 编码字符串
* @return 解密后的明文字符串
* @throws Exception 如果解密过程中发生错误
*/
public static String decrypt(String encryptedText) throws Exception {
// 创建一个带填充的缓冲块密码器,使用 CBC 模式和 SM4 引擎
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()));
// 初始化密码器为解密模式,并设置密钥和初始化向量
cipher.init(false, new ParametersWithIV(new KeyParameter(KEY.getBytes()), IV.getBytes()));
// 将 Base64 编码的密文解码为字节数组
byte[] input = Base64.getDecoder().decode(encryptedText);
// 计算输出缓冲区的大小
byte[] output = new byte[cipher.getOutputSize(input.length)];
// 进行分步解密
int length1 = cipher.processBytes(input, 0, input.length, output, 0);
int length2 = cipher.doFinal(output, length1);
// 返回解密后的明文字符串
return new String(output, 0, length1 + length2);
}
}
三、构建过滤器 SmCryptoFilter
过滤器的作用及其在Spring Boot中的配置方式
过滤器(Filter)是Java Servlet规范的一部分,允许开发人员在请求到达Servlet或控制器之前或者响应返回给客户端之前执行预处理和后处理逻辑。对于SmCryptoFilter
而言,它的主要作用是对加密的请求体进行解密,以便后续处理器能够直接处理原始数据。
要在Spring Boot中配置过滤器,可以通过以下两种方式之一实现:
- 通过注解:使用
@Component
注解将过滤器类标记为Spring管理的Bean,并实现javax.servlet.Filter
接口。 - 通过注册Bean:创建一个方法,该方法返回一个新的过滤器实例,并使用
@Bean
注解将其注册到Spring应用上下文中。
SmCryptoFilter实现
import com.example.demo.utils.CustomRequestWrapper;
import com.example.demo.utils.Sm4Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.ContentCachingResponseWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
public class SmCryptoFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(SmCryptoFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// 对请求进行解密
String decryptedRequestBody = decryptRequest(httpRequest);
// 处理解密后的请求体 CustomRequestWrapper为自定义请求包装器,请看下面实现
CustomRequestWrapper customRequestWrapper = new CustomRequestWrapper(httpRequest, decryptedRequestBody);
// 继续处理请求
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse);
chain.doFilter(customRequestWrapper, responseWrapper);
// 获取并加密响应内容
byte[] content = responseWrapper.getContentAsByteArray();
String originalResponseBody = new String(content);
String encryptedResponseBody = encryptResponse(originalResponseBody);
// 写回客户端
responseWrapper.reset();
responseWrapper.getWriter().write(encryptedResponseBody);
responseWrapper.copyBodyToResponse();
} catch (Exception e) {
logger.error("内部响应错误:{}", e.getMessage());
e.printStackTrace();
// 返回标准错误响应
httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
httpResponse.getWriter().write("Internal Server Error");
}
}
/**
* 解密 HTTP 请求的主体内容。
*
* @param request HTTP 请求对象
* @return 解密后的请求体字符串
* @throws IOException 如果读取或解密过程中发生错误
*/
private String decryptRequest(HttpServletRequest request) throws IOException {
StringBuilder jb = new StringBuilder();
String line;
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null) {
jb.append(line);
}
try {
return Sm4Util.decrypt(jb.toString());
} catch (Exception e) {
throw new IOException("Decryption failed", e);
}
}
/**
* 加密 HTTP 响应的主体内容。
*
* @param response 原始响应体字符串
* @return 加密后的响应体字符串
*/
private String encryptResponse(String response) {
try {
return Sm4Util.encrypt(response);
} catch (Exception e) {
return "Encryption failed: " + e.getMessage();
}
}
}
CustomRequestWrapper
为什么需要自定义请求封装器?
在 Java Servlet 编程中,当一个 HTTP 请求到达服务器时,Servlet 容器会创建一个 HttpServletRequest
对象来表示这个请求。对于 POST 请求或 PUT 请求等包含请求体的请求,可以通过 getInputStream()
或 getReader()
方法来读取请求体内容。然而,由于性能和资源管理的原因,Servlet 规范规定了请求体只能被读取一次
如果我们的应用程序中有多个组件需要访问请求体(例如过滤器、拦截器或控制器),那么这个问题就可能显现出来。比如,如果我们在一个过滤器中读取了请求体,并且试图在控制器中再次读取它,就会触发下面异常。
jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalStateException: getReader() has already been called for this request
实现
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private final String body;
/**
* 构造函数,用于创建自定义请求包装器。
*
* @param request 原始 HTTP 请求对象
* @param body 新的请求体内容
* @throws IOException 如果读取或处理请求体时发生错误
*/
public CustomRequestWrapper(HttpServletRequest request, String body) throws IOException {
super(request);
this.body = body;
}
/**
* 重写 getInputStream 方法,返回一个新的 ServletInputStream,
* 其中包含修改后的请求体内容。
*
* @return 包含修改后请求体内容的 ServletInputStream
* @throws IOException 如果读取输入流时发生错误
*/
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
// 返回是否已经完成读取
return false;
}
@Override
public boolean isReady() {
// 返回是否准备好读取数据
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// 设置读监听器,这里不需要实现
}
@Override
public int read() throws IOException {
// 从字节数组输入流中读取下一个字节
return byteArrayInputStream.read();
}
};
}
/**
* 重写 getReader 方法,返回一个新的 BufferedReader,
* 其中包含修改后的请求体内容。
*
* @return 包含修改后请求体内容的 BufferedReader
* @throws IOException 如果读取输入流时发生错误
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
- 如何拦截特定URL模式下的请求?
为了使过滤器仅应用于特定的URL模式,可以在注册过滤器时指定URL映射。这里我们选择通过@Bean的方式注册过滤器,可以这样做:
import com.example.demo.filter.SmCryptoFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<SmCryptoFilter> loggingFilter(){
FilterRegistrationBean<SmCryptoFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SmCryptoFilter());
registrationBean.addUrlPatterns("/api/*"); // 根据实际需求调整URL模式
// registrationBean.addInitParameter("excludedUrls","/api/*");
return registrationBean;
}
}
上述配置表示只有匹配/api/*
路径模式的请求才会被SmCryptoFilter
拦截并处理。这种方式非常灵活,可以根据需要调整拦截规则,从而精确控制哪些请求应该经过加密解密流程。
四、测试接口 TestController
- 创建一个简单的RESTful API作为测试用例
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/api")
public class TestController {
@PostMapping("/data")
public String processData(@RequestBody String encryptedRequest) {
try {
// 解密整个请求体
System.out.println("过滤器解密后发送的请求体: " + encryptedRequest);
return "hello world";
} catch (Exception e) {
throw new RuntimeException("解密或处理消息失败", e);
}
}
}
调用示例
控制层接收结果
响应结果
五、总结
在整个实现过程中,我们通过构建和配置SmCryptoFilter
过滤器,成功地为Spring Boot应用添加了对加密请求的支持。接下来,我们将回顾整个实现过程,讨论可能遇到的问题及解决方案。
1. 回顾整个实现过程
SmCryptoFilter
过滤器:这个组件作为整个加密解密流程的核心,负责拦截匹配特定URL模式的请求,在请求到达控制器之前使用Sm4Util
工具类对请求体进行解密处理。它确保了敏感信息在传输过程中保持安全。Sm4Util
工具类:提供具体的加密和解密功能,是基于SM4算法实现的。为了保证数据的安全性,所有的加密操作都应通过该工具类完成。CustomHttpServletRequestWrapper
:用于包装原始请求对象,以包含解密后的请求体。这是因为一旦请求被读取,它的输入流不能被重复使用,因此需要一个包装器来保存解密的数据供后续处理器使用。- 过滤器注册与配置:通过Spring Boot提供的灵活配置方式,可以很容易地将自定义过滤器注册到应用中,并指定其作用范围(即哪些URL模式应该被过滤)。
这些组件相互协作,共同构成了一个完整的加密解密处理链路,确保了应用中敏感数据的安全传输。
2. 可能遇到的问题及解决方案
- 请求体只能读取一次:这是因为在Servlet规范中,请求的输入流只能被读取一次。解决方案是使用如
CustomHttpServletRequestWrapper
这样的包装器来保存请求体的内容。 - 加密算法的选择与实现:选择合适的加密算法对于确保数据安全性至关重要。虽然本示例中使用了SM4算法,但在实际项目中,可能需要根据具体需求支持更多类型的加密算法。
- 性能问题:加密和解密操作可能会带来额外的性能开销。优化策略包括但不限于采用更高效的加密算法、合理设置缓存等。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库