1. 项目概述
在前后端分离的Web开发中,接口数据安全一直是个重要话题。最近有读者问到如何在Spring Boot中优雅地实现接口参数加解密,这让我想起在实际项目中遇到的几个典型场景:
- 移动端APP与后端API通信时,需要防止关键业务数据被明文抓包
- 某些敏感接口需要确保请求参数在传输过程中不被篡改
- 合规性要求强制对特定数据字段进行加密传输
传统的做法是在每个Controller中手动调用加解密工具类,但这样会导致大量重复代码。更优雅的方案是利用Spring MVC提供的扩展点实现全局加解密。本文将详细介绍基于RequestBodyAdvice和ResponseBodyAdvice的解决方案,并封装成可复用的Starter。
2. 技术方案设计
2.1 核心思路解析
实现接口加解密主要有三种技术路线:
-
过滤器/拦截器方案:
- 优点:可以获取原始HTTP请求和响应流
- 缺点:需要处理各种异常情况,开发复杂度高
-
AOP切面方案:
- 优点:可以精确控制特定方法的加解密
- 缺点:无法直接操作请求/响应体
-
Advice增强方案:
- RequestBodyAdvice:在参数转换前处理请求体
- ResponseBodyAdvice:在返回数据序列化前处理响应
- 优点:与Spring MVC深度集成,开发简单
- 缺点:仅支持@RequestBody和@ResponseBody场景
经过综合比较,我们选择第三种方案,因为:
- 前后端分离项目普遍使用JSON交互
- Spring原生支持,稳定性有保障
- 通过注解可以灵活控制加解密范围
2.2 加密算法选型
常见的加密方案对比如下:
| 加密类型 | 算法示例 | 密钥长度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 对称加密 | AES | 128/256位 | 高 | 大数据量加密 |
| 非对称加密 | RSA | 2048位 | 低 | 密钥交换、小数据加密 |
| 哈希算法 | SHA-256 | 256位 | 中 | 数据完整性校验 |
选择AES-256的原因:
- 加解密性能好,适合接口频繁调用的场景
- Java内置支持,无需额外依赖
- ECB模式简单易实现(生产环境建议使用CBC或GCM模式)
安全提示:实际项目中应考虑结合HTTPS使用,并定期轮换加密密钥。本文示例为演示原理使用ECB模式和固定密钥,生产环境需要调整。
3. 核心实现细节
3.1 基础工具类封装
首先实现AES加解密工具类:
java复制public class AESUtils {
private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
private static Cipher getCipher(byte[] key, int model) throws Exception {
// 密钥长度检查
if(key.length != 16 && key.length != 32) {
throw new IllegalArgumentException("Invalid AES key length");
}
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(model, secretKeySpec);
return cipher;
}
public static String encrypt(byte[] data, byte[] key) throws Exception {
Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
byte[] encrypted = cipher.doFinal(data);
return Base64.getEncoder().encodeToString(encrypted);
}
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
byte[] decoded = Base64.getDecoder().decode(data);
return cipher.doFinal(decoded);
}
}
关键点说明:
- 添加密钥长度校验,确保符合AES规范
- 使用Base64编码加密结果,保证可读性
- PKCS5Padding提供标准的填充方案
3.2 自定义注解设计
定义两个注解控制加解密行为:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface Decrypt {
String value() default ""; // 可扩展指定不同密钥
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
String[] exclude() default {}; // 排除不需要加密的字段
}
注解设计考虑:
- @Decrypt支持方法级和参数级使用
- @Encrypt预留字段排除功能,增强灵活性
- 未来可扩展支持不同的加密策略
3.3 请求解密实现
继承RequestBodyAdviceAdapter实现解密逻辑:
java复制@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
@Autowired
private EncryptProperties encryptProperties;
@Override
public boolean supports(MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return parameter.hasMethodAnnotation(Decrypt.class) ||
parameter.hasParameterAnnotation(Decrypt.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
byte[] bodyBytes = StreamUtils.copyToByteArray(inputMessage.getBody());
try {
byte[] decrypted = AESUtils.decrypt(bodyBytes, encryptProperties.getKey().getBytes());
return new ByteArrayHttpInputMessage(decrypted, inputMessage.getHeaders());
} catch (Exception e) {
throw new DecryptFailException("Decryption failed", e);
}
}
static class ByteArrayHttpInputMessage implements HttpInputMessage {
private final byte[] body;
private final HttpHeaders headers;
// 构造方法和接口实现
}
}
关键处理逻辑:
- 读取原始请求体字节数据
- 调用AES工具类解密
- 重新构造HttpInputMessage供后续处理
- 自定义异常处理解密失败情况
3.4 响应加密实现
实现ResponseBodyAdvice接口完成加密:
java复制@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
@Autowired
private EncryptProperties encryptProperties;
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(Encrypt.class);
}
@Override
public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
try {
byte[] keyBytes = encryptProperties.getKey().getBytes();
if (body.getMsg() != null) {
body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(), keyBytes));
}
if (body.getObj() != null) {
String json = objectMapper.writeValueAsString(body.getObj());
body.setObj(AESUtils.encrypt(json.getBytes(), keyBytes));
}
return body;
} catch (Exception e) {
throw new EncryptFailException("Encryption failed", e);
}
}
}
注意事项:
- 只处理@Encrypt注解的方法
- 分别加密消息体和业务对象
- 对象需要先序列化为JSON再加密
- 统一处理加密异常
4. Starter封装与配置
4.1 自动化配置类
java复制@Configuration
@EnableConfigurationProperties(EncryptProperties.class)
@ConditionalOnWebApplication
public class EncryptAutoConfiguration {
@Bean
public EncryptResponse encryptResponse() {
return new EncryptResponse();
}
@Bean
public DecryptRequest decryptRequest() {
return new DecryptRequest();
}
}
4.2 配置属性类
java复制@ConfigurationProperties(prefix = "spring.encrypt")
public class EncryptProperties {
private String key = "default-encrypt-key-16";
private String algorithm = "AES/ECB/PKCS5Padding";
// getters and setters
}
配置说明:
- 默认密钥长度必须符合AES要求
- 支持通过application.yml自定义配置
- 预留算法配置项供扩展
4.3 注册自动配置
META-INF/spring.factories内容:
code复制org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.encrypt.starter.EncryptAutoConfiguration
5. 使用示例与测试
5.1 引入依赖
xml复制<dependency>
<groupId>com.example</groupId>
<artifactId>encrypt-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
5.2 接口定义
java复制@RestController
public class UserController {
@Encrypt
@GetMapping("/user/{id}")
public RespBean getUser(@PathVariable Long id) {
User user = userService.findById(id);
return RespBean.ok("success", user);
}
@PostMapping("/user")
public RespBean createUser(@RequestBody @Decrypt User user) {
User saved = userService.save(user);
return RespBean.ok("created", saved);
}
}
5.3 测试案例
java复制@SpringBootTest
class UserControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testGetUser() {
ResponseEntity<String> response = restTemplate.getForEntity("/user/1", String.class);
String encrypted = response.getBody();
// 验证返回数据是加密的
assertTrue(isBase64(encrypted));
}
@Test
void testCreateUser() {
User user = new User("test", "test@example.com");
String encrypted = AESUtils.encrypt(user.toJson().getBytes(), key);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(encrypted, headers);
ResponseEntity<User> response = restTemplate.postForEntity("/user", request, User.class);
assertEquals(200, response.getStatusCodeValue());
}
}
6. 生产环境注意事项
-
密钥管理:
- 不要将密钥硬编码在代码中
- 推荐使用KMS或Vault等密钥管理系统
- 定期轮换加密密钥
-
性能优化:
- 加解密操作会带来性能开销,建议:
- 对非敏感接口禁用加解密
- 使用连接池重用Cipher实例
- 考虑异步加解密方案
- 加解密操作会带来性能开销,建议:
-
安全增强:
- 结合HTTPS使用
- 添加请求签名验证
- 实现防重放攻击机制
-
异常处理:
- 定义明确的加解密异常类型
- 提供友好的错误提示
- 记录详细的错误日志
-
监控指标:
- 统计加解密成功率
- 监控加解密耗时
- 设置异常告警阈值
7. 扩展思路
-
多加密算法支持:
- 通过策略模式支持AES、RSA等不同算法
- 根据注解参数动态选择加密方式
-
字段级加解密:
- 结合Jackson的JsonSerializer实现
- 使用注解标记需要加密的字段
-
前端配合方案:
- 提供JavaScript加解密工具库
- 实现Web端密钥协商机制
-
性能优化方案:
- 引入缓存减少重复加解密开销
- 支持批量加解密操作
-
密钥动态获取:
- 集成密钥管理系统API
- 实现密钥自动轮换机制
在实际项目中,我们基于这个starter进行了二次开发,增加了请求签名验证和防重放攻击功能。一个重要的经验是:加解密只是安全链条中的一环,需要与其他安全措施配合使用才能构建真正安全的系统。