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

  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算法,但在实际项目中,可能需要根据具体需求支持更多类型的加密算法。
  • 性能问题:加密和解密操作可能会带来额外的性能开销。优化策略包括但不限于采用更高效的加密算法、合理设置缓存等。
posted @   Comfortable  阅读(266)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示