前言
在实际项目中,接口数据的加解密是保护敏感信息的重要手段。传统的做法是在每个接口手动调用加解密工具类,这种方式代码侵入性强、维护成本高。
本文将分享一套基于自定义注解 + 过滤器 的接口加解密方案,支持 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 四种主流加密算法 |
| 灵活配置 | 支持方法级、类级配置,可自定义密钥 |
| 无缝切换 | 可通过配置开关灵活启用/禁用加解密功能 |
| 易于扩展 | 基于处理器工厂模式,新增算法只需实现接口 |
七、注意事项
- 密钥安全:生产环境密钥应通过加密配置中心管理,不建议直接写在配置文件中
- 性能考虑:频繁加解密会影响性能,建议仅在敏感接口使用
- 异常处理:加解密失败时统一返回错误信息,避免暴露敏感数据
- SM4 依赖:使用 SM4 需要注册 BouncyCastle Provider
八、总结
本文介绍了一套完整的 SpringBoot 接口加解密解决方案,核心思路是:
- 注解驱动 :通过
@EncryptDecrypt注解标记需要加解密的接口 - 过滤器拦截:在过滤器层统一处理请求解密和响应加密
- 策略模式:通过工厂类动态获取不同算法的处理器
- 包装类支持:通过 RequestWrapper/ResponseWrapper 实现请求响应的多次读取