如何在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的工作流程主要由以下几个步骤组成:

  1. 密钥扩展:将初始的128位密钥扩展成一个包含32个32位字的扩展密钥数组。这个过程包括S盒变换、线性变换以及轮函数的应用,目的是生成一系列子密钥以供后续加密使用。
  2. 轮函数:每一轮都应用了一个复杂的轮函数,该函数包含了四个主要操作:
    • 非线性变换(S盒):通过查找表的方式实现,提供良好的混淆效果。
    • 线性变换:基于矩阵运算,增加输出的复杂度和不可预测性。
    • 轮密钥加:将当前轮的子密钥与状态进行异或操作,增强安全性。
    • 字节重排:调整字节顺序,进一步扰乱数据结构。
  3. 加密过程:输入的明文首先被分成四个32位的字,然后依次经过32轮轮函数的处理,最终得到对应的密文。
  4. 解密过程:解密本质上是加密的逆过程。区别在于使用的轮密钥顺序相反,但轮函数本身保持不变。

应用场景

由于其高效且安全的特点,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算法,但在实际项目中,可能需要根据具体需求支持更多类型的加密算法。
  • 性能问题:加密和解密操作可能会带来额外的性能开销。优化策略包括但不限于采用更高效的加密算法、合理设置缓存等。

如何在Spring Boot项目中添加国密SM4加密支持?——基于过滤器的实现的更多相关文章

  1. spring boot学习02【如何在spring boot项目中访问jsp】

    1.配置application.properties文件 打开application.properties追加 spring.mvc.view.prefix=/WEB-ROOT/ spring.mvc ...

  2. 如何在Spring Boot项目中巧妙利用策略模式干掉if else!

    直入主题 我们都知道,设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路.它不是语法规定,而是一套用来提高代码可复用性.可维护性.可读性.稳健性以及安全性 ...

  3. 如何在Spring Boot项目中集成微信支付V3

    Payment Spring Boot 是微信支付V3的Java实现,仅仅依赖Spring内置的一些类库.配置简单方便,可以让开发者快速为Spring Boot应用接入微信支付. 演示例子: paym ...

  4. Spring Boot项目中使用Swagger2

    Swagger2是一款restful接口文档在线生成和在线接口调试工具,Swagger2在Swagger1.x版本的基础上做了些改进,下面是在一个Spring Boot项目中引入Swagger2的简要 ...

  5. 在Spring Boot项目中使用Spock框架

    转载:https://www.jianshu.com/p/f1e354d382cd Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring B ...

  6. Spring Boot项目中使用Mockito

    本文首发于个人网站:Spring Boot项目中使用Mockito Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试:生成测试数据初始化数据库用于测试 ...

  7. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  8. Spring Boot项目中如何定制拦截器

    本文首发于个人网站:Spring Boot项目中如何定制拦截器 Servlet 过滤器属于Servlet API,和Spring关系不大.除了使用过滤器包装web请求,Spring MVC还提供Han ...

  9. Spring Boot项目中如何定制servlet-filters

    本文首发于个人网站:Spring Boot项目中如何定制servlet-filters 在实际的web应用程序中,经常需要在请求(request)外面增加包装用于:记录调用日志.排除有XSS威胁的字符 ...

  10. Spring Boot项目中MyBatis连接DB2和MySQL数据库返回结果中一些字符消失——debug笔记

    写这篇记录的原因是因为我之前在Spring Boot项目中通过MyBatis连接DB2返回的结果中存在一些字段, 这些字段的元素中缺少了一些符号,所以我现在通过在自己的电脑上通过MyBatis连接DB ...

随机推荐

  1. CF926 Div.2

    C. Sasha and the Casino 赌场规则:如果下注 \(y(y > 0)\) 元,如果赢了则除了 \(y\) 元外,额外获得 \(y \times (k - 1)\) 元,否则则 ...

  2. git 推送代码到多个 远端仓库

    业务场景 在开发代码时,有时希望将代码推送到两个远端仓库. 实现方法 git remote add origin giturl1 git remote add backup giturl2 git p ...

  3. SaaS架构中多租户的概念

    SaaS架构中多租户的概念 租户可以理解为部署在云端的客户,通常出现在2B的企业中,比如现在学校的一卡通管理,通常是一个公司来做的,学校本地不需要做任何部署,而这个公司又是服务了很多个学校,那么学校对 ...

  4. Windows 触控笔

    平板以及二合一平板均是触控屏,Laptop现在也有很多屏幕带触控 触控屏,都会配置触控笔配件,目前市场上一般是电容屏+电容笔的技术方案. 触控笔分为主动笔和被动笔,主动笔占绝大部分.主动笔是通过内部电 ...

  5. DA14531芯片固件逆向系列(1)-固件加载和逆向分析

    首发于先知论坛 https://xz.aliyun.com/t/9185 前言 本文介绍逆向DA14531芯片的固件,并介绍一些辅助自动化分析的脚本的实现.DA14531是Dialog公司研制的蓝牙芯 ...

  6. 零基础学习人工智能—Python—Pytorch学习(十二)

    前言 本文介绍使用神经网络进行实战. 使用的代码是<零基础学习人工智能-Python-Pytorch学习(九)>里的代码. 代码实现 mudule定义 首先我们自定义一个module,创建 ...

  7. shell 将文件内容读取到 数组中

    #!/bin/bash prod_file=/home/vmuser/linbo/kettleDemo/job/test/CA-20201224.csv test_file=/home/vmuser/ ...

  8. Arch Linux 安装完成后配置声音

    安装完 Arch Linux 后,虽然已经装了 alsa-utils,但是仍然可能出现无法播放声音的情况,这里记录了一种解决方案,在我的 Dell 上成功. 如果使用 alsamixer 解除静音后还 ...

  9. Alpine中安装telnet

    lpine Linux是一个基于musl libc和busybox的安全轻量的Linux发行版. 在Alpine中安装telnet,并不是apk add telnettelnet被移入子包busybo ...

  10. 使用Apache commons-pool2实现高效的FTPClient连接池的方法

    一. 连接池概述​ 频繁的建立和关闭连接,会极大的降低系统的性能,而连接池会在初始化的时候会创建一定数量的连接,每次访问只需从连接池里获取连接,使用完毕后再放回连接池,并不是直接关闭连接,这样可以保证 ...