在实际项目中,很多接口数据会通过 HTTP 明文传输,尤其是内部系统或移动端 App 的接口。即使部署了 HTTPS,应用层加密仍然可以作为第二道防线,避免日志或网关层意外记录明文敏感信息。Spring Boot 整合 AES 对称加密并不复杂,核心思路是利用过滤器或拦截器在请求到达 Controller 之前完成密文解密,在响应返回客户端之前完成明文加密。整个过程对业务代码几乎透明,只需要前端配合使用相同的加密算法和密钥即可。

为什么选择 AES 以及模式选型
AES(Advanced Encryption Standard)是目前应用最广泛的对称加密算法,加解密使用同一个密钥,效率远高于 RSA 等非对称算法,适合对接口请求体和响应体进行批量加密。在 Java 中可以直接使用 javax.crypto 包下的 Cipher 类操作 AES,不需要额外引入第三方依赖。Spring Boot 项目通常已经具备 JDK 基础环境,因此整合成本很低。
AES 支持多种加密模式,如 ECB、CBC、GCM、CTR 等。ECB 模式最为简单,但相同明文块会产生相同密文块,安全性较差,不建议在接口加密中使用。CBC 模式通过引入 IV(Initialization Vector)使相同的明文每次加密结果不同,能够抵抗模式分析攻击,是接口加密中比较常见的选择。GCM 模式则在加密的同时提供完整性校验,可以防止密文被篡改,但实现稍复杂且对 JDK 版本有要求。综合考虑,如果只是做接口数据加密传输,CBC + PKCS5Padding 足够,密钥和 IV 都使用 16 字节长度即可。
另一个容易忽略的点是密钥存储。不要把 AES 密钥直接硬编码在类文件中,一旦源码泄露密钥就会失效。推荐通过环境变量、配置中心或 KMS 服务获取密钥,并在启动时校验密钥长度不为空。本文示例为了简单会从配置文件中读取,实际生产环境应替换为更安全的管理方式。
封装 AES 加解密工具类
先创建一个工具类 AESUtil,负责生成密钥、加密、解密以及 Base64 编解码。Base64 用于将二进制密文转换为可传输的字符串,否则直接放在 JSON 或表单中容易出现乱码。工具类需要设置算法为 AES/CBC/PKCS5Padding,并指定 IV 参数。下面给出一个兼容前后端 Java 和 JavaScript 的简单实现。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AESUtil {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String KEY_ALGORITHM = "AES";
// 16字节密钥,实际项目请从配置中心获取
private static final String SECRET_KEY = "1234567890123456";
// 16字节IV,实际项目请从配置中心获取
private static final String IV_STRING = "abcdef9876543210";
public static String encrypt(String content) throws Exception {
if (content == null || content.isEmpty()) {
return content;
}
byte[] keyBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(IV_STRING.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
public static String decrypt(String encryptedContent) throws Exception {
if (encryptedContent == null || encryptedContent.isEmpty()) {
return encryptedContent;
}
byte[] keyBytes = SECRET_KEY.getBytes(StandardCharsets.UTF_8);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(IV_STRING.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedContent);
byte[] decrypted = cipher.doFinal(encryptedBytes);
return new String(decrypted, StandardCharsets.UTF_8);
}
}
上述代码中,密钥和 IV 都写成了常量,便于阅读。生产环境建议通过 @Value 注入配置项,并确保配置项不以明文形式出现在 Git 仓库中。加密方法在内容为空时原样返回,这是为了避免解密空字符串时抛出异常,影响接口正常运行。Base64 编码后的字符串长度会比原始明文增加约三分之一,因此在设计请求体大小时需要预留空间。
除了工具类,还需要考虑多线程并发下的安全性。Cipher 实例不是线程安全的,所以每次加解密都创建新的 Cipher 对象,避免共享同一个实例导致的数据错乱。虽然创建对象的开销稍大,但相比网络传输的耗时可以忽略不计。
通过过滤器实现请求解密与响应加密
要让接口自动完成加解密,最直接的方式是注册一个 OncePerRequestFilter。在过滤器中读取请求体密文,解密后重新包装请求,再把包装后的请求继续传递给后续的过滤器链和 Controller。响应加密则需要包装 HttpServletResponse,在写回数据前对输出内容进行加密。
首先创建一个请求包装器 DecryptRequestWrapper,继承 HttpServletRequestWrapper,重写 getInputStream 和 getReader 方法,用解密后的字节数组替代原始请求体。读取原始请求体时要注意使用 StreamUtils 或类似工具读取字节,避免一次性读取导致后续无法处理。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class DecryptRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public DecryptRequestWrapper(HttpServletRequest request, byte[] body) {
super(request);
this.body = body;
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
}
响应包装器 EncryptResponseWrapper 继承 HttpServletResponseWrapper,内部使用 ByteArrayOutputStream 捕获输出流。当 Controller 写入响应后,过滤器取出捕获到的明文字节,调用 AESUtil.encrypt 加密,再写入原始响应的输出流。
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.IOException;
import java.io.PrintWriter;
public class EncryptResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private ServletOutputStream servletOutputStream;
private PrintWriter printWriter;
public EncryptResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (servletOutputStream == null) {
servletOutputStream = new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
};
}
return servletOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (printWriter == null) {
printWriter = new PrintWriter(getOutputStream());
}
return printWriter;
}
public byte[] getResponseData() {
return byteArrayOutputStream.toByteArray();
}
}
过滤器 AESFilter 在 doFilterInternal 中判断请求路径是否需要加密。实际项目中可以配置白名单,例如登录接口、文件上传接口或健康检查接口可以跳过。对于需要加密的请求,先读取请求体密文,调用工具类解密,构造包装器并放行。接着对响应进行包装,在请求处理完成后取出明文响应,加密后写回。
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.util.StreamUtils;
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;
@Component
public class AESFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUri = request.getRequestURI();
// 根据实际需求配置需要加密的路径,这里示例仅对 /api/ 前缀生效
if (!requestUri.startsWith("/api/")) {
filterChain.doFilter(request, response);
return;
}
// 请求解密
byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream());
DecryptRequestWrapper requestWrapper = new DecryptRequestWrapper(request, requestBody);
String decryptedBody = null;
try {
decryptedBody = AESUtil.decrypt(new String(requestBody, StandardCharsets.UTF_8));
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid encrypted request");
return;
}
DecryptRequestWrapper decryptedRequest = new DecryptRequestWrapper(request, decryptedBody.getBytes(StandardCharsets.UTF_8));
// 响应加密
EncryptResponseWrapper responseWrapper = new EncryptResponseWrapper(response);
filterChain.doFilter(decryptedRequest, responseWrapper);
byte[] responseData = responseWrapper.getResponseData();
String plainResponse = new String(responseData, StandardCharsets.UTF_8);
try {
String encryptedResponse = AESUtil.encrypt(plainResponse);
response.setContentType("text/plain;charset=UTF-8");
response.getOutputStream().write(encryptedResponse.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("Encryption failed");
}
}
}
上面的过滤器逻辑有一个小问题:第一次读取请求体后没有把原始请求体传递给后续,而是用解密后的请求体替代。实际上可以直接在读取原始请求体后先解密,再用解密后的字节数组构造包装器,不需要创建两次包装器。这里为清晰展示流程做了拆分,实际开发中可以简化。另外 StreamUtils.copyToByteArray 需要引入 spring-core 依赖,Spring Boot 项目默认已经包含。
请求解密时假设前端发送的请求体就是单纯的一段密文字符串,而不是 JSON 包着一层密文字段。这种方式对前端要求较低,直接把整个 body 加密后发送即可。如果希望保留 JSON 结构,可以在前端把密文放入某个字段,过滤器再读取字段值解密,但这样会增加复杂度。接口加密适用于 POST/PUT 等带请求体的方法,GET 请求的参数也可以加密后放入 query string,过滤器需要额外处理 request.getParameter 的解密。
测试接口加密与常见问题排查
要验证过滤器是否生效,可以写一个简单的 Controller 接收密文并返回明文。接口路径以 /api/ 开头,这样 AESFilter 才会处理。前端使用 JavaScript 的 CryptoJS 库进行 AES-CBC 加密,密钥和 IV 与后端保持一致,加密结果用 Base64 编码后作为请求体发送。
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;
@RestController
@RequestMapping("/api")
public class TestController {
@PostMapping("/encrypt")
public String testEncrypt(@RequestBody String requestBody) {
// 这里收到的 requestBody 已经是解密后的明文
System.out.println("Received plain text: " + requestBody);
return "Hello, encrypted world";
}
}
前端调用时,使用 CryptoJS 的 AES.encrypt 方法,指定 CryptoJS.mode.CBC 和 CryptoJS.pad.Pkcs7,密钥和 IV 需要转换为 CryptoJS.enc.Utf8.parse 格式。响应返回的密文同样可以用 CryptoJS 解密查看。如果前后端加解密结果不一致,优先检查密钥和 IV 是否完全一致,然后是 Base64 编码格式,最后确认填充模式是否匹配。常见错误包括密钥长度不是 16/24/32 字节、IV 长度不是 16 字节、或者把密钥和 IV 的值写反。
另一个常见问题是过滤器的顺序。Spring Boot 中 OncePerRequestFilter 默认执行顺序由注册方式决定,如果项目里还有其他过滤器读取请求体或响应内容,可能会与 AESFilter 冲突。可以通过 @Order 注解或 FilterRegistrationBean 调整顺序。另外,如果接口使用了 @RequestBody 接收 JSON 对象,解密后的请求体必须是合法的 JSON 字符串,过滤器不会替你做 JSON 解析,所以前端加密前的原始数据必须是一个格式正确的 JSON 字符串。
性能方面,AES 加解密本身速度很快,每毫秒可以处理数 KB 数据,对普通接口的响应时间影响通常在几毫秒以内。更需要注意的是大响应体的 Base64 编码会增加约三分之一的数据量,如果响应体本来就有几十 KB,加密后的网络传输会明显变慢。可以在过滤器中对响应体大小设置阈值,超过阈值时改用流式加密或提示前端接受二进制数据,不过这会增加实现复杂度,一般业务场景不需要考虑。
总结起来,Spring Boot 整合 AES 接口加密的核心步骤是:封装加解密工具类、编写请求和响应包装器、注册过滤器处理指定路径、与前端约定密钥与 IV。通过这种方式可以在不修改业务代码的前提下为接口数据增加一层对称加密保护,适合对数据安全有一定要求但暂时无法全面升级 HTTPS 的项目。
Spring BootAES加密接口数据加密修改时间:2026-09-21 11:11:33