本文目录导读:

在 Java 中统一处理响应加密流程,最常见的做法是使用 Spring 的 ResponseBodyAdvice 或者 过滤器/拦截器 结合 自定义注解 来实现。
核心目标是:在业务代码不感知的情况下,对所有(或特定)接口的响应体进行统一的加密处理。
下面给出一个基于 Spring Boot + ResponseBodyAdvice + JSON 序列化的最佳实践流程。
核心思路流程图
Client Request
|
v
Controller (业务逻辑) -> 返回 Object (如 UserVO, Result<Data>)
|
v
Spring 的 ResponseBodyAdvice (拦截)
| 1. 判断是否需要加密 (是否包含 @Encrypt 注解 或 配置了全局加密)
| 2. 如果是,将 Object 转为 JSON String (如 Jackson)
| 3. 使用 AES/国密等算法加密 JSON String 得到密文
| 4. 返回一个新的 "加密响应体" (如 EncryptResponse)
|
v
HttpMessageConverter (序列化 EncryptResponse 为 JSON)
|
v
Client (收到 {"data":"Base64EncryptedString","code":0,"msg":"success"})
逐步实现方案
定义加密响应体的数据结构(推荐方案)
客户端需要解析,一种流行做法是:外层结构不变,data 字段变成一个字符串(密文)。
// 1. 定义一个通用的加密响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EncryptResponse {
private int code;
private String msg;
private String data; // 注意:这里 data 是 String 类型的密文
}
编写响应加密工具类(以 AES 为例)
// 2. 加密工具类
public class CryptoUtil {
private static final String KEY = "Your32ByteSecretKeyHere!"; // 必须是16/24/32字节
public static String encrypt(String plainText) {
// 使用 AES/GCM/NoPadding 或 AES/CBC/PKCS5Padding
// 返回 Base64 编码后的密文
// 具体实现略 (可以使用 Spring 的 Encryptors 或 Hutool)
return "encrypted_" + plainText; // 简化示例
}
}
实现 ResponseBodyAdvice(核心)
这是统一加密的入口,实现 ResponseBodyAdvice 接口,在 beforeBodyWrite 方法中处理。
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 1. 判断是否应该拦截:检查方法或类上是否有 @Encrypt 注解
// 如果有,返回 true(表示支持处理),否则返回 false
return returnType.hasMethodAnnotation(Encrypt.class)
|| returnType.getContainingClass().isAnnotationPresent(Encrypt.class)
// 或者判断是否为全局加密:这里返回 true 表示所有接口都加密
// || true;
;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 2. body 已经是 EncryptResponse 类型,说明已经被加密过,直接返回
if (body instanceof EncryptResponse) {
return body;
}
// 3. 特殊情况处理:如果返回的是 String 类型,情况复杂,建议避免。
// Spring 默认的 StringHttpMessageConverter 会覆盖 body。
// 简单处理:body 是 String,单独加密
if (body instanceof String) {
String encrypted = CryptoUtil.encrypt((String) body);
return new EncryptResponse(0, "success", encrypted);
}
// 4. 核心逻辑:将 body(任意 POJO 或 Result<Data>)序列化为 JSON String
try {
// 4.1 使用 Jackson 将 Object 转为 JSON String
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(body);
// 4.2 对 JSON String 进行加密
String encryptedData = CryptoUtil.encrypt(jsonString);
// 4.3 包装成统一的加密响应体
return new EncryptResponse(0, "success", encryptedData);
} catch (Exception e) {
log.error("加密失败", e);
// 加密失败时,返回错误响应(不要泄露原始数据)
return new EncryptResponse(-1, "encryption error", "");
}
}
}
定义自定义注解(可选,用于控制哪些接口加密)
// 自定义注解,标记需要加密的接口
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
}
在 Controller 中使用
@RestController
public class UserController {
@GetMapping("/user")
// 标记此接口响应需要加密
@Encrypt
public Result<UserVO> getUser() {
UserVO user = new UserVO("张三", "123456");
return Result.success(user);
}
@GetMapping("/public/info")
// 不加 @Encrypt,响应为明文
public String getPublicInfo() {
return "Hello World";
}
}
测试效果
- 访问
/user:返回{"code":0,"msg":"success","data":"加密后的用户JSON字符串"} - 访问
/public/info:返回Hello World(未加密)
复杂情况与最佳实践
应对 body 是 String 类型的特殊情况
Spring 的 StringHttpMessageConverter 优先级高于 MappingJackson2HttpMessageConverter,如果你的 Controller 直接返回 String,beforeBodyWrite 中的 body 是 String 类型,但 Spring 会尝试用 StringHttpMessageConverter 输出,此时如果你返回了一个 EncryptResponse 对象,StringHttpMessageConverter 无法处理,会报错。
解决方案:
- 最佳实践:统一返回
Result<Object>或ResponseEntity,避免 Controller 直接返回String。 - 如果必须:在
beforeBodyWrite中判断selectedConverterType是否为StringHttpMessageConverter,如果是,手动设置Content-Type为application/json,并直接写入密文字符串。
// 在 beforeBodyWrite 中
if (body instanceof String) {
// 强制让 Spring 使用 MappingJackson2HttpMessageConverter
// 但更简单的是:直接返回一个新对象,并确保 mediaType 是 application/json
String encrypted = CryptoUtil.encrypt((String) body);
// 返回一个包含字符串的普通对象,而不是 EncryptResponse
// 或者直接用 ObjectMapper 转为 JSON
return new EncryptResponse(0, "success", encrypted);
}
性能考量(重要)
- 避免重复序列化:在
ResponseBodyAdvice中使用ObjectMapper将body转为 JSON,然后加密,最后又将EncryptResponse转为 JSON,这导致了 两次序列化,在 QPS 较高的场景下,这会增加 CPU 开销和响应延迟。 - 优化方案:
- 自定义
AbstractHttpMessageConverter:直接处理加密逻辑,将原始对象序列化为 JSON 流后进行加密,然后输出密文,这避免了中间创建大字符串。 - 或者让 Controller 返回
String,但如前所述,这不太优雅。
- 自定义
密钥管理
- 不要硬编码密钥:将密钥配置在
application.yml或配置中心(如 Nacos、Apollo)中。 - 定期轮换密钥:支持多密钥版本。
- 使用非对称加密交换密钥:通常方案是使用 RSA 加密 AES 的密钥,客户端和服务端各自持有 RSA 公钥/私钥,确保密钥传输安全。
与 @RestControllerAdvice 结合
@ControllerAdvice (ResponseBodyAdvice) 和 @RestControllerAdvice (@ExceptionHandler) 可以同时使用,加密应在异常处理之后进行。
客户端解密流程
作为参考,客户端收到密文后,需要:
- 解析外层 JSON (
EncryptResponse)。 - 提取
data字段的 Base64 字符串。 - 使用相同的密钥和算法解密,得到 JSON String。
JSON.parse()得到原始数据对象。
- 使用
ResponseBodyAdvice是 Java Spring 生态中最标准的统一响应加密方案。 - 关键点:处理好
String返回值类型、避免二次序列化、动态决定是否加密(通过注解)。 - 安全底线:加密失败一定不能返回明文数据,应返回错误码。
这种方法可以让业务开发人员完全不知晓加密过程,只专注于业务逻辑,实现了加密与业务的解耦。