SpringBoot 自定义注解实现接口加解密:一套完整的多算法方案

前言

在实际项目中,接口数据的加解密是保护敏感信息的重要手段。传统的做法是在每个接口手动调用加解密工具类,这种方式代码侵入性强、维护成本高。

本文将分享一套基于自定义注解 + 过滤器 的接口加解密方案,支持 AES、RSA、DES、SM4 四种算法,通过注解即可实现请求参数自动解密、响应数据自动加密,代码零侵入,配置灵活。


一、方案整体架构

1.1 核心设计思路

复制代码
┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│   客户端     │───▶│  加解密Filter │───▶│   Controller │
└─────────────┘    └──────────────┘    └─────────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │ 加解密处理器  │
                    │  (Processor) │
                    └──────────────┘
                           │
        ┌──────────────────┼──────────────────┐
        ▼                  ▼                  ▼
  ┌─────────┐        ┌─────────┐        ┌─────────┐
  │   AES   │        │   RSA   │        │  SM4... │
  └─────────┘        └─────────┘        └─────────┘

1.2 核心组件

组件 作用
@EncryptDecrypt 注解 标记需要加解密的接口,配置算法类型
EncryptDecryptFilter 过滤器拦截请求/响应,执行加解密逻辑
CryptoProcessor 接口 加解密处理器统一接口
CryptoProcessorFactory 处理器工厂,根据类型获取对应处理器
RequestWrapper/ResponseWrapper 请求/响应包装类,支持多次读取

二、项目依赖配置

2.1 Maven 依赖

xml 复制代码
<!-- BouncyCastle 用于 SM4 国密算法支持 -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

<!-- Hutool 工具类(可选,密钥生成工具用到) -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.20</version>
</dependency>

<!-- Spring Boot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2 配置文件

yaml 复制代码
crypto:
  # 是否启用加解密过滤器
  filter:
    enabled: true

  # AES配置
  aes:
    key: "4ltKCIQnppKYu+Rnd0gVuA=="  # 32字节Base64编码
    iv: "klkDgHpzOQXvmClS1OMIxg=="  # 16字节Base64编码

  # RSA配置
  rsa:
    public-key: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0iL9tZrrAsAp8QW1VuuKdxryCjV0WVw4r88hFNh/CTjggLqZAQusISH0vdpJxA5wgjpHSTYcTR1GaXjjOKaikT9sAF4uJRp6t8YNRvl1ttphRo7XbAix3wdka4meUnqrk0gKALzvADnvoGjS6xh+UHqzQyhoNlRQwX3mv9mCUDwIDAQAB"
    private-key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALSIv21musCwCnxBbVW64p3GvIKNXRZXDivzyEU2H8JOOCAupkBC6whIfS92knEDnCCOkdJNhxNHUZpeOM4pqKRP2wAXi4lGnq3xg1G+XW22mFGjtdsCLHfB2RriZ5SequTSAoAvO8AOe+gaNLrGH5QerNDKGg2VFDBfea/2YJQPAgMBAAECgYAMgZyhO4icaJWRXgbw9W1VeLr+YESek5FLwiuRf9pbX4fL0u95bClT/ZGOhCxI3/Mk3juD0hMVZs9kXjqZLZGdEmJoZotjFY6oWZWq/FeYj2jJmVbXV5LLk/E02TNA0YWTcGdzNuR4ItxaZbz/nwJzmgitNsP2LlneNizdglBRQQJBAPBiJ5ugqbaJz2XxPGQat503oF8buwFbkt6dDRq5eYshuCngWTGLIGHSFNSvR9ZGg6U7ZBkZUJAgSv/psdJpolcCQQDAQzusMoGuNQP5ExD0KobKIih24AHeaWoIobuLOg8UE9V7iXncjURdPfDtI7V5USK3dCxw4VVcdq8DXUL6z7kJAkAK+mCWsequRHLtU+wPIk06Z9zyGwEaWcVGV2PO0aOkYADaIL3SDmmHLiH3aJ3eQlAmzqSOpOJSAreKInEVi+93AkBqsaTe1ZRzqYYP5g3FgggVCkmsVnmTBKrMKXybMdgGS09wZAVVaKvklqgp4WQm8+ixJ+41okatrktHMa0m5LmJAkEApiY1LVCBHfKtpCDFrHnnY0XKjexjL6M11f226ump6CKf6GqopuGCwJ0Fn6YIHxGgJacO3MdFbvNDjKn85PvzdQ=="

  # DES配置
  des:
    key: "m4b7ZBAIcPs="  # 8字节Base64编码

  # SM4配置
  sm4:
    key: "IxNgZwpjH98rB0jLHhIBhA=="  # 16字节Base64编码
    iv: "QAkG3KrYWrywOEMTYpMWUA=="  # 16字节Base64编码

2.3 BouncyCastle Provider 注册

在实现 WebMvcConfigurer 接口的配置类中添加:

java 复制代码
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.annotation.PostConstruct;
import java.security.Security;

@Component
public class WebMvcConfig implements WebMvcConfigurer {

    @PostConstruct
    public void registerBouncyCastle() {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
            System.out.println("✅ Bouncy Castle Provider registered.");
        }
    }
}

三、核心代码实现

3.1 配置属性类

java 复制代码
package com.xxx.common.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "crypto")
@Data
public class CryptoProperties {
    private boolean enabled = true;
    private AesConfig aes;
    private RsaConfig rsa;
    private DesConfig des;
    private Sm4Config sm4;

    @Data
    public static class AesConfig {
        private String key;
        private String iv;
    }

    @Data
    public static class RsaConfig {
        private String publicKey;
        private String privateKey;
    }

    @Data
    public static class DesConfig {
        private String key;
    }

    @Data
    public static class Sm4Config {
        private String key;
        private String iv;
    }
}

3.2 加解密类型枚举

java 复制代码
// 加密类型枚举
public enum EncryptType {
    NONE,       // 不加密
    AES,        // AES加密
    RSA,        // RSA加密
    DES,        // DES加密
    SM4,        // 国密SM4
    CUSTOM      // 自定义
}

// 解密类型枚举
public enum DecryptType {
    NONE,       // 不解密
    AES,        // AES解密
    RSA,        // RSA解密
    DES,        // DES解密
    SM4,        // 国密SM4
    CUSTOM      // 自定义
}

3.3 自定义注解

java 复制代码
package com.xxx.common.encryptor.annotation;

import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;

import java.lang.annotation.*;

/**
 * 加解密注解
 * 可标注在方法或类上
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EncryptDecrypt {
    // 请求参数解密类型
    DecryptType requestDecrypt() default DecryptType.NONE;

    // 响应参数加密类型
    EncryptType responseEncrypt() default EncryptType.NONE;

    // 自定义密钥(可选)
    String secretKey() default "";

    // 加解密策略
    String strategy() default "default";
}

3.4 加解密处理器接口

java 复制代码
package com.xxx.common.core;

import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;

/**
 * 加解密处理器接口
 */
public interface CryptoProcessor {
    String encrypt(String data, String key) throws Exception;
    String decrypt(String encryptedData, String key) throws Exception;
    String getAlgorithmName();
    boolean supports(DecryptType decryptType, EncryptType encryptType);
}

3.5 AES 加解密处理器

java 复制代码
package com.xxx.common.core.encryptor;

import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import com.xxx.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
 * AES加解密处理器
 */
@Component
@Slf4j
public class AesCryptoProcessor implements CryptoProcessor {
    private static final String ALGORITHM = "AES";
    private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";

    @Value("${crypto.aes.key}")
    private String defaultKey;

    @Value("${crypto.aes.iv}")
    private String iv;

    @Override
    public String encrypt(String data, String key) throws Exception {
        if (StringUtils.isEmpty(key)) {
            key = defaultKey;
         }
        byte[] keyBytes = Base64.getDecoder().decode(key);
        byte[] ivBytes = Base64.getDecoder().decode(iv); // ✅ 关键:解码 IV
        SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);

        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    @Override
    public String decrypt(String encryptedData, String key) throws Exception {
        if (StringUtils.isEmpty(key)) {
            key = defaultKey;
        }
        byte[] keyBytes = Base64.getDecoder().decode(key);
        byte[] ivBytes = Base64.getDecoder().decode(iv); // ✅ 关键:解码 IV
        SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);

        byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(original, StandardCharsets.UTF_8);
    }

    @Override
    public String getAlgorithmName() {
        return "AES";
    }

    @Override
    public boolean supports(DecryptType decryptType, EncryptType encryptType) {
        return decryptType == DecryptType.AES || encryptType == EncryptType.AES;
    }
}

3.6 RSA 加解密处理器

java 复制代码
package com.xxx.common.core.encryptor;

import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import com.xxx.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
 * RSA加解密处理器  加密不要太长的数据 如果需要可以 RSA+AES 结合使用 RES加解密 AES的 key和 iv 核心参数加解密使用的还是 AES
 */
@Component
@Slf4j
public class RsaCryptoProcessor implements CryptoProcessor {
    private static final String ALGORITHM = "RSA";

    @Value("${crypto.rsa.public-key}")
    private String publicKey;

    @Value("${crypto.rsa.private-key}")
    private String privateKey;

    @Override
    public String encrypt(String data, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? publicKey : key;

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(useKey));

        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    @Override
    public String decrypt(String encryptedData, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? privateKey : key;

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(useKey));

        byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(original, StandardCharsets.UTF_8);
    }

    private PublicKey getPublicKey(String key) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(key);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        return keyFactory.generatePublic(spec);
    }

    private PrivateKey getPrivateKey(String key) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(key);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
        return keyFactory.generatePrivate(spec);
    }

    @Override
    public String getAlgorithmName() {
        return "RSA";
    }

    @Override
    public boolean supports(DecryptType decryptType, EncryptType encryptType) {
        return decryptType == DecryptType.RSA || encryptType == EncryptType.RSA;
    }
}

3.7 DES 加解密处理器

java 复制代码
package com.xxx.common.core.encryptor;

import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import com.xxx.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
 * DES加解密处理器
 */
@Component
@Slf4j
public class DesCryptoProcessor implements CryptoProcessor {
    private static final String ALGORITHM = "DES";
    private static final String TRANSFORMATION = "DES/ECB/PKCS5Padding";

    @Value("${crypto.des.key}")
    private String defaultKey;

    @Override
    public String encrypt(String data, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? defaultKey : key;

        byte[] keyBytes = Base64.getDecoder().decode(useKey);
        SecretKey secretKey = new SecretKeySpec(keyBytes, ALGORITHM);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);

        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    @Override
    public String decrypt(String encryptedData, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? defaultKey : key;

        byte[] keyBytes = Base64.getDecoder().decode(useKey);
        SecretKey secretKey = new SecretKeySpec(keyBytes, ALGORITHM);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);

        byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(original, StandardCharsets.UTF_8);
    }

    @Override
    public String getAlgorithmName() {
        return "DES";
    }

    @Override
    public boolean supports(DecryptType decryptType, EncryptType encryptType) {
        return decryptType == DecryptType.DES || encryptType == EncryptType.DES;
    }
}

3.8 SM4 国密加解密处理器

java 复制代码
package com.xxx.common.core.encryptor;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import com.xxx.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
 * SM4加解密处理器
 */
@Component
@Slf4j
public class Sm4CryptoProcessor implements CryptoProcessor {
    private static final String ALGORITHM = "SM4";
    private static final String TRANSFORMATION = "SM4/CBC/PKCS5Padding";

    @Value("${crypto.sm4.key}")
    private String defaultKey;

    @Value("${crypto.sm4.iv}")
    private String defaultIv;

    @Override
    public String encrypt(String data, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? defaultKey : key;
        String useIv = defaultIv;
        return encryptWithIv(data, useKey, useIv);
    }

    @Override
    public String decrypt(String encryptedData, String key) throws Exception {
        String useKey = StringUtils.isEmpty(key) ? defaultKey : key;
        String useIv = defaultIv;
        return decryptWithIv(encryptedData, useKey, useIv);
    }

    public String encryptWithIv(String data, String key, String iv) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(key);
        byte[] ivBytes = StringUtils.isEmpty(iv) ?
                new byte[16] : Base64.getDecoder().decode(iv);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
        SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

        Map<String, String> result = new HashMap<>();
        result.put("iv", Base64.getEncoder().encodeToString(ivBytes));
        result.put("data", Base64.getEncoder().encodeToString(encrypted));

        return Base64.getEncoder().encodeToString(
                new ObjectMapper().writeValueAsString(result).getBytes());
    }

    public String decryptWithIv(String encryptedData, String key, String iv) throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(key);

        String decodedStr = new String(Base64.getDecoder().decode(encryptedData));
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonNode = mapper.readTree(decodedStr);

        byte[] ivBytes;
        byte[] dataBytes;

        if (jsonNode.has("iv") && jsonNode.has("data")) {
            ivBytes = Base64.getDecoder().decode(jsonNode.get("iv").asText());
            dataBytes = Base64.getDecoder().decode(jsonNode.get("data").asText());
        } else {
            ivBytes = StringUtils.isEmpty(iv) ?
                    Base64.getDecoder().decode(defaultIv) : Base64.getDecoder().decode(iv);
            dataBytes = Base64.getDecoder().decode(encryptedData);
        }

        Cipher cipher = Cipher.getInstance(TRANSFORMATION, "BC");
        SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
        byte[] original = cipher.doFinal(dataBytes);
        return new String(original, StandardCharsets.UTF_8);
    }

    @Override
    public String getAlgorithmName() {
        return "SM4";
    }

    @Override
    public boolean supports(DecryptType decryptType, EncryptType encryptType) {
        return decryptType == DecryptType.SM4 || encryptType == EncryptType.SM4;
    }
}

3.9 处理器工厂

java 复制代码
package com.xxx.common.core.encryptor;

import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class CryptoProcessorFactory {
    @Autowired
    private List<CryptoProcessor> processors;

    private final Map<String, CryptoProcessor> processorCache = new ConcurrentHashMap<>();

    public CryptoProcessor getProcessor(DecryptType decryptType, EncryptType encryptType) {
        String key = decryptType.name() + "_" + encryptType.name();

        return processorCache.computeIfAbsent(key, k ->
                processors.stream()
                        .filter(p -> p.supports(decryptType, encryptType))
                        .findFirst()
                        .orElseThrow(() -> new IllegalArgumentException(
                                String.format("No processor found for decryptType: %s, encryptType: %s",
                                        decryptType, encryptType)))
        );
    }
}

3.10 请求/响应包装类

RequestWrapper.java

java 复制代码
package com.xxx.common.filter;

import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class RequestWrapper extends HttpServletRequestWrapper {
    private byte[] body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                // do nothing
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    public byte[] getBody() {
        return body;
    }

    public String getBodyAsString() {
        return new String(body, StandardCharsets.UTF_8);
    }

    public void setBody(String body) {
        this.body = body.getBytes(StandardCharsets.UTF_8);
    }
}

ResponseWrapper.java

java 复制代码
package com.xxx.common.filter;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

public class ResponseWrapper extends HttpServletResponseWrapper {
    private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    private PrintWriter writer = new PrintWriter(outputStream);

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return new ServletOutputStream() {
            @Override
            public void write(int b) {
                outputStream.write(b);
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {
                // do nothing
            }
        };
    }

    @Override
    public PrintWriter getWriter() {
        return writer;
    }

    public byte[] getContentAsBytes() {
        if (writer != null) {
            writer.flush();
        }
        return outputStream.toByteArray();
    }

    public String getContentAsString() {
        return new String(getContentAsBytes(), StandardCharsets.UTF_8);
    }
}

3.11 加解密过滤器

java 复制代码
package com.xxx.common.filter;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.common.core.CryptoProcessor;
import com.xxx.common.core.encryptor.CryptoProcessorFactory;
import com.xxx.common.encryptor.annotation.EncryptDecrypt;
import com.xxx.common.enums.DecryptType;
import com.xxx.common.enums.EncryptType;
import com.xxx.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class EncryptDecryptFilter extends OncePerRequestFilter {
    @Autowired
    private CryptoProcessorFactory cryptoProcessorFactory;

    @Autowired
    private ObjectMapper objectMapper;

    @Value("${crypto.filter.enabled}")
    private boolean enabled;

    @Autowired
    private List<HandlerMapping> handlerMappings;

    private static final ThreadLocal<EncryptDecryptInfo> ENCRYPT_INFO_THREAD_LOCAL = new ThreadLocal<>();

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        if (!enabled) {
            filterChain.doFilter(request, response);
            return;
        }

        // 1. 预解析HandlerMethod
        HandlerMethod handlerMethod = preResolveHandlerMethod(request);
        if (handlerMethod == null) {
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 获取加解密注解
        EncryptDecrypt encryptDecrypt = getEncryptDecryptAnnotation(handlerMethod);
        if (encryptDecrypt == null ||
                (encryptDecrypt.requestDecrypt() == DecryptType.NONE &&
                        encryptDecrypt.responseEncrypt() == EncryptType.NONE)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 3. 存储注解信息到ThreadLocal
        ENCRYPT_INFO_THREAD_LOCAL.set(new EncryptDecryptInfo(encryptDecrypt));

        // 4. 创建包装对象
        EncryptDecryptHttpServletRequestWrapper requestWrapper = null;
        EncryptDecryptHttpServletResponseWrapper responseWrapper = null;

        try {
            // 5. 处理请求解密
            if (encryptDecrypt.requestDecrypt() != DecryptType.NONE) {
                requestWrapper = new EncryptDecryptHttpServletRequestWrapper(request);
                decryptRequest(requestWrapper, encryptDecrypt);
            } else {
                requestWrapper = new EncryptDecryptHttpServletRequestWrapper(request);
            }

            // 6. 包装响应
            responseWrapper = new EncryptDecryptHttpServletResponseWrapper(response);

            // 7. 继续过滤器链
            filterChain.doFilter(
                    requestWrapper != null ? requestWrapper : request,
                    responseWrapper != null ? responseWrapper : response
            );

            // 8. 处理响应加密
            if (encryptDecrypt.responseEncrypt() != EncryptType.NONE && responseWrapper != null) {
                encryptResponse(responseWrapper, response, encryptDecrypt);
            }

        } catch (Exception e) {
            log.error("加解密处理失败", e);
            handleCryptoError(response, e);
        } finally {
            ENCRYPT_INFO_THREAD_LOCAL.remove();
        }
    }

    private HandlerMethod preResolveHandlerMethod(HttpServletRequest request) {
        for (HandlerMapping handlerMapping : handlerMappings) {
            try {
                HandlerExecutionChain handler = handlerMapping.getHandler(request);
                if (handler != null && handler.getHandler() instanceof HandlerMethod) {
                    return (HandlerMethod) handler.getHandler();
                }
            } catch (Exception e) {
                log.debug("HandlerMapping解析异常: {}", e.getMessage());
            }
        }
        return null;
    }

    private void decryptRequest(EncryptDecryptHttpServletRequestWrapper requestWrapper,
                                EncryptDecrypt encryptDecrypt) throws Exception {
        String requestBody = requestWrapper.getBodyAsString();
        if (StringUtils.isEmpty(requestBody)) {
            return;
        }

        CryptoProcessor processor = cryptoProcessorFactory.getProcessor(
                encryptDecrypt.requestDecrypt(), EncryptType.NONE);

        try {
            JsonNode jsonNode = objectMapper.readTree(requestBody);
            if (jsonNode.has("encryptedData")) {
                String encryptedData = jsonNode.get("encryptedData").asText();
                String decryptedData = processor.decrypt(encryptedData, encryptDecrypt.secretKey());
                requestWrapper.setBody(decryptedData);
            } else if (jsonNode.isTextual()) {
                String decryptedData = processor.decrypt(requestBody, encryptDecrypt.secretKey());
                requestWrapper.setBody(decryptedData);
            }
        } catch (Exception e) {
            String decryptedData = processor.decrypt(requestBody, encryptDecrypt.secretKey());
            requestWrapper.setBody(decryptedData);
        }
    }

    private void encryptResponse(EncryptDecryptHttpServletResponseWrapper responseWrapper,
                                 HttpServletResponse originalResponse,
                                 EncryptDecrypt encryptDecrypt) throws Exception {
        String responseBody = responseWrapper.getContentAsString();
        if (StringUtils.isEmpty(responseBody)) {
            return;
        }

        CryptoProcessor processor = cryptoProcessorFactory.getProcessor(
                DecryptType.NONE, encryptDecrypt.responseEncrypt());

        String encryptedData = processor.encrypt(responseBody, encryptDecrypt.secretKey());

        Map<String, Object> result = new HashMap<>();
        result.put("encrypted", true);
        result.put("algorithm", processor.getAlgorithmName());
        result.put("timestamp", System.currentTimeMillis());
        result.put("data", encryptedData);

        String finalResponse = objectMapper.writeValueAsString(result);

        originalResponse.reset();
        originalResponse.setContentType("application/json;charset=UTF-8");
        originalResponse.setContentLength(finalResponse.getBytes(StandardCharsets.UTF_8).length);

        originalResponse.getWriter().write(finalResponse);
        originalResponse.getWriter().flush();
    }

    private EncryptDecrypt getEncryptDecryptAnnotation(HandlerMethod handlerMethod) {
        EncryptDecrypt methodAnnotation = handlerMethod.getMethodAnnotation(EncryptDecrypt.class);
        if (methodAnnotation != null) {
            return methodAnnotation;
        }
        return handlerMethod.getBeanType().getAnnotation(EncryptDecrypt.class);
    }

    private void handleCryptoError(HttpServletResponse response, Exception e) throws IOException {
        response.reset();
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> errorResult = new HashMap<>();
        errorResult.put("code", 400);
        errorResult.put("message", "加解密处理失败: " + e.getMessage());
        errorResult.put("timestamp", System.currentTimeMillis());

        response.getWriter().write(objectMapper.writeValueAsString(errorResult));
        response.getWriter().flush();
    }

    public static class EncryptDecryptHttpServletRequestWrapper extends RequestWrapper {
        public EncryptDecryptHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
        }
    }

    public static class EncryptDecryptHttpServletResponseWrapper extends ResponseWrapper {
        private boolean isCommited = false;

        public EncryptDecryptHttpServletResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        @Override
        public PrintWriter getWriter() {
            if (!isCommited) {
                return super.getWriter();
            }
            throw new IllegalStateException("Response already committed");
        }

        @Override
        public void flushBuffer() throws IOException {
            isCommited = true;
            super.flushBuffer();
        }
    }

    private static class EncryptDecryptInfo {
        final EncryptDecrypt encryptDecrypt;

        EncryptDecryptInfo(EncryptDecrypt encryptDecrypt) {
            this.encryptDecrypt = encryptDecrypt;
        }
    }
}

3.12 自动配置类

java 复制代码
package com.xxx.common.core;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.common.filter.EncryptDecryptFilter;
import com.xxx.common.properties.CryptoProperties;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;

@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(CryptoProperties.class)
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class EncryptDecryptAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public FilterRegistrationBean<EncryptDecryptFilter> encryptDecryptFilterRegistration(
            EncryptDecryptFilter filter) {

        FilterRegistrationBean<EncryptDecryptFilter> registration =
                new FilterRegistrationBean<>(filter);
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
        registration.addUrlPatterns("/*");
        registration.setName("encryptDecryptFilter");
        registration.setEnabled(true);

        return registration;
    }

    @Bean
    @ConditionalOnMissingBean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return mapper;
    }
}

四、使用示例

4.1 Controller 使用

java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {

    // 请求用AES解密,响应用AES加密
    @PostMapping("/login")
    @EncryptDecrypt(requestDecrypt = DecryptType.AES, responseEncrypt = EncryptType.AES)
    public Result<UserInfo> login(@RequestBody LoginRequest request) {
        // 正常业务逻辑,无需手动解密
        UserInfo userInfo = userService.login(request);
        return Result.success(userInfo);
    }

    // 仅响应加密(请求不加密)
    @GetMapping("/info")
    @EncryptDecrypt(responseEncrypt = EncryptType.RSA)
    public Result<UserInfo> getUserInfo(@RequestParam String userId) {
        UserInfo userInfo = userService.getById(userId);
        return Result.success(userInfo);
    }

    // 类级别配置,所有接口统一使用SM4
    @EncryptDecrypt(requestDecrypt = DecryptType.SM4, responseEncrypt = EncryptType.SM4)
    public class SensitiveDataController {
        @PostMapping("/save")
        public Result<Void> save(@RequestBody SensitiveData data) {
            // ...
        }
    }
}

4.2 请求格式

客户端发送加密请求(AES解密示例):

json 复制代码
{
  "encryptedData": "这里是Base64编码后的AES加密数据"
}

或者直接发送加密字符串(支持两种格式)。

4.3 响应格式

加密响应示例:

json 复制代码
{
  "encrypted": true,
  "algorithm": "AES",
  "timestamp": 1737997967890,
  "data": "这里是Base64编码后的AES加密数据"
}

五、密钥生成工具

java 复制代码
package com.xxx.access.utils;

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.KeyUtil;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

public class CryptoKeyGenerator {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    public static Map<String, String> generateAesCbcKeyMaterial() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(128);
        SecretKey secretKey = keyGen.generateKey();

        byte[] iv = new byte[16];
        SECURE_RANDOM.nextBytes(iv);

        Map<String, String> result = new HashMap<>();
        result.put("algorithm", "AES/CBC/PKCS5Padding");
        result.put("key", Base64.encode(secretKey.getEncoded()));
        result.put("iv", Base64.encode(iv));
        return result;
    }

    public static Map<String, String> generateDesEcbKeyMaterial() throws NoSuchAlgorithmException {
        KeyGenerator keyGen = KeyGenerator.getInstance("DES");
        SecretKey secretKey = keyGen.generateKey();

        Map<String, String> result = new HashMap<>();
        result.put("algorithm", "DES/ECB/PKCS5Padding");
        result.put("key", Base64.encode(secretKey.getEncoded()));
        return result;
    }

    public static Map<String, String> generateSm4CbcKeyMaterial() {
        SecretKey secretKey = KeyUtil.generateKey("SM4");
        byte[] key = secretKey.getEncoded();

        byte[] iv = new byte[16];
        SECURE_RANDOM.nextBytes(iv);

        Map<String, String> result = new HashMap<>();
        result.put("algorithm", "SM4/CBC/PKCS5Padding");
        result.put("key", Base64.encode(key));
        result.put("iv", Base64.encode(iv));
        return result;
    }

    public static Map<String, String> generateRsaKeyPair() {
        cn.hutool.crypto.asymmetric.RSA rsa = new cn.hutool.crypto.asymmetric.RSA();
        Map<String, String> result = new HashMap<>();
        result.put("algorithm", "RSA");
        result.put("privateKey", rsa.getPrivateKeyBase64());
        result.put("publicKey", rsa.getPublicKeyBase64());
        return result;
    }

    public static Map<String, String> generateByKeyType(String algorithm) throws NoSuchAlgorithmException {
        switch (algorithm.toUpperCase()) {
            case "AES":
                return generateAesCbcKeyMaterial();
            case "DES":
                return generateDesEcbKeyMaterial();
            case "SM4":
                return generateSm4CbcKeyMaterial();
            case "RSA":
                return generateRsaKeyPair();
            default:
                throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== AES-CBC ===");
        System.out.println(generateByKeyType("AES"));

        System.out.println("\n=== DES-ECB ===");
        System.out.println(generateByKeyType("DES"));

        System.out.println("\n=== SM4-CBC ===");
        System.out.println(generateByKeyType("SM4"));

        System.out.println("\n=== RSA ===");
        System.out.println(generateByKeyType("RSA"));
    }
}

六、方案优势

特性 说明
零代码侵入 通过注解即可实现加解密,业务代码无需改动
多算法支持 支持 AES、RSA、DES、SM4 四种主流加密算法
灵活配置 支持方法级、类级配置,可自定义密钥
无缝切换 可通过配置开关灵活启用/禁用加解密功能
易于扩展 基于处理器工厂模式,新增算法只需实现接口

七、注意事项

  1. 密钥安全:生产环境密钥应通过加密配置中心管理,不建议直接写在配置文件中
  2. 性能考虑:频繁加解密会影响性能,建议仅在敏感接口使用
  3. 异常处理:加解密失败时统一返回错误信息,避免暴露敏感数据
  4. SM4 依赖:使用 SM4 需要注册 BouncyCastle Provider

八、总结

本文介绍了一套完整的 SpringBoot 接口加解密解决方案,核心思路是:

  • 注解驱动 :通过 @EncryptDecrypt 注解标记需要加解密的接口
  • 过滤器拦截:在过滤器层统一处理请求解密和响应加密
  • 策略模式:通过工厂类动态获取不同算法的处理器
  • 包装类支持:通过 RequestWrapper/ResponseWrapper 实现请求响应的多次读取
相关推荐
m0_748708053 小时前
C++代码移植性设计
开发语言·c++·算法
Σίσυφος19003 小时前
特征值分解eig
人工智能·算法
子豪-中国机器人3 小时前
2026年1月31日特长测试常见难点
算法
zhengfei6113 小时前
踪有趣的 Linux(和 UNIX)恶意软件。提交 PR
java·数据库·mysql
「QT(C++)开发工程师」3 小时前
C++ 观察者模式
java·c++·观察者模式
m0_706653233 小时前
高性能网络协议栈
开发语言·c++·算法
永远睡不够的入3 小时前
类和对象(上)
开发语言·c++·算法
想进个大厂3 小时前
代码随想录day31 贪心05
数据结构·算法·leetcode
yyy(十一月限定版)3 小时前
寒假集训1——暴力和枚举
数据结构·算法