Java响应加密流程如何统一

wen java案例 29

本文目录导读:

Java响应加密流程如何统一

  1. 核心思路流程图
  2. 逐步实现方案
  3. 复杂情况与最佳实践

在 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(未加密)

复杂情况与最佳实践

应对 bodyString 类型的特殊情况

Spring 的 StringHttpMessageConverter 优先级高于 MappingJackson2HttpMessageConverter,如果你的 Controller 直接返回 StringbeforeBodyWrite 中的 bodyString 类型,但 Spring 会尝试用 StringHttpMessageConverter 输出,此时如果你返回了一个 EncryptResponse 对象,StringHttpMessageConverter 无法处理,会报错。

解决方案

  1. 最佳实践:统一返回 Result<Object>ResponseEntity,避免 Controller 直接返回 String
  2. 如果必须:在 beforeBodyWrite 中判断 selectedConverterType 是否为 StringHttpMessageConverter,如果是,手动设置 Content-Typeapplication/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 中使用 ObjectMapperbody 转为 JSON,然后加密,最后又将 EncryptResponse 转为 JSON,这导致了 两次序列化,在 QPS 较高的场景下,这会增加 CPU 开销和响应延迟。
  • 优化方案
    1. 自定义 AbstractHttpMessageConverter:直接处理加密逻辑,将原始对象序列化为 JSON 流后进行加密,然后输出密文,这避免了中间创建大字符串。
    2. 或者让 Controller 返回 String,但如前所述,这不太优雅。

密钥管理

  • 不要硬编码密钥:将密钥配置在 application.yml 或配置中心(如 Nacos、Apollo)中。
  • 定期轮换密钥:支持多密钥版本。
  • 使用非对称加密交换密钥:通常方案是使用 RSA 加密 AES 的密钥,客户端和服务端各自持有 RSA 公钥/私钥,确保密钥传输安全。

@RestControllerAdvice 结合

@ControllerAdvice (ResponseBodyAdvice) 和 @RestControllerAdvice (@ExceptionHandler) 可以同时使用,加密应在异常处理之后进行。

客户端解密流程

作为参考,客户端收到密文后,需要:

  1. 解析外层 JSON (EncryptResponse)。
  2. 提取 data 字段的 Base64 字符串。
  3. 使用相同的密钥和算法解密,得到 JSON String。
  4. JSON.parse() 得到原始数据对象。
  1. 使用 ResponseBodyAdvice 是 Java Spring 生态中最标准的统一响应加密方案。
  2. 关键点:处理好 String 返回值类型、避免二次序列化、动态决定是否加密(通过注解)。
  3. 安全底线:加密失败一定不能返回明文数据,应返回错误码。

这种方法可以让业务开发人员完全不知晓加密过程,只专注于业务逻辑,实现了加密与业务的解耦。

抱歉,评论功能暂时关闭!