1、背景
在项目做等保,要求需要对接口数据进行加密,所做了调用方发送请求前,先对明文加密,然后发送密文,被调用方收到数据后,先进行解密,然后再进行处理。返回的结果同样也可以加密,被调用方将需要返回的数据进行加密,然后将密文返回给调用方,调用方收到后,再进行解密便得到明文。
2、解决方案
使用AES的方式对数据加密
AES介绍
- 对称加密
加密与解密使用同一密钥,效率高但需安全分发密钥。 - 分组密码
将明文分割为固定128位(16字节)的块独立处理。 - 密钥长度灵活
支持128位、192位、256位三种长度,安全性依次递增(AES-128/192/256)
利用RequestBodyAdviceAdapter,继承这个类重写其beforeBodyRead方法,完成请求在进入方法前对参数解密
RequestBodyAdviceAdapter介绍
RequestBodyAdviceAdapter 是 Spring Framework 中用于拦截和处理 **@RequestBody
注解参数**的核心组件,属于 Spring MVC 的全局增强机制。它通过 AOP 思想实现对请求体的统一预处理,避免在 Controller 中重复编写与业务无关的逻辑(如解密、校验、日志记录等)
方法 |
调用时机 |
用途 |
返回值 |
|
请求进入时优先判断 |
决定当前 Advice 是否生效(如按包路径、注解过滤) |
|
|
请求体被 读取前 |
修改原始请求体(如解密、字符编码转换) |
自定义的 |
|
请求体转换为 Java 对象后 |
修改对象(如参数校验、字段注入) |
处理后的 Java 对象 |
|
请求体为空时 |
处理空请求体场景(如设置默认值) |
替代空体的对象 |
在数据返回时候需要加密,用到ResponseBodyAdvice
ResponseBodyAdvice 介绍
ResponseBodyAdvice 是 Spring MVC(4.1+)及 Spring Boot 中用于全局拦截并定制化处理响应体的核心接口,通常与 @ControllerAdvice
或 @RestControllerAdvice
配合使用。它允许开发者在控制器方法执行后、响应数据写入 HTTP 响应体之前,对返回的数据进行统一处理,适用于多种通用场景(如数据包装、脱敏、日志记录等)。
- 定位与触发时机
-
- 作用阶段:在控制器方法执行完毕且返回值被
HttpMessageConverter
序列化之前介入。 - 触发条件:仅对标注
@ResponseBody
或@RestController
的控制器方法生效。
- 作用阶段:在控制器方法执行完毕且返回值被
- 核心方法
方法 |
作用 |
参数说明 |
|
判断当前响应是否需被处理(返回 则触发 ) |
|
|
实际处理响应体,可修改或替换原始返回值 |
|
作用方法指定方案
1.用到了@RestControllerAdvice这个注解指定扫描包路径来实现,这样就需要supports()
方法直接返回true
@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")
2.如果想指定具体方法,可以自定义注解方式,在supports()方法下指定注解
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Decrypt.class);
}
3.代码实现
指定配置类讲AES注册到ioc
@Configuration
@EnableConfigurationProperties(SecureProperties.class)
public class SecureConfiguration {
@Autowired
private SecureProperties secureProperties;
@Bean
public AES aes() {
return SecureUtil.aes(this.secureProperties.getKey().getBytes(StandardCharsets.UTF_8));
}
}
配置
@Getter
@Setter
@ConfigurationProperties("secure")
public class SecureProperties {
/**
* 秘钥(长度只能是 128、192或256位,一个普通字符是8位)
*/
private String key;
}
yml
secure:
key: 1234567890123456
入参加密
/**
* 请求到达controller中的方法之前,会拦截标注有 @Decrypt 注解或者指定包下的方法,负责将原始请求中的密文转换为明文
*/
@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")
public class DecryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
@Autowired
private AES aes;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 注解方式
//return methodParameter.hasMethodAnnotation(Decrypt.class);
//路径方式直接返回true
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
String encoding = "UTF-8";
try {
//①:获取http请求中原始的body
String body = IOUtils.toString(inputMessage.getBody(), encoding);
//②:解密body,使用AES算法解密,得到明文
String decryptBody = aes.decryptStr(body);
//将解密之后的body数据重新封装为HttpInputMessage作为当前方法的返回值
InputStream inputStream = new ByteArrayInputStream(decryptBody.getBytes(encoding));
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return inputStream;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
// 如果解密失败,返回原始消息
return inputMessage;
}
}
}
/**
* 在结果返回给调用者之前,会拦截标注有@Encrypt 注解或者包下方法的方法,对接口的返回值进行处理,将其转换为密文
*/
@ControllerAdvice(basePackages = "cn.shenzhihe.collection.controller")
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private AES aes;
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 注解方式
//return methodParameter.hasMethodAnnotation(Decrypt.class);
//路径方式直接返回true
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
return body;
}
String result;
if (body instanceof String) {
result = (String) body;
} else {
//如果是对象,则转换为json字符串
try {
result = objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
//加密返回
return this.aes.encryptHex(result);
}
}
两个注解一起使用,入参解密,返回加密
注解
/**
* 接口方法上添加该注解,这表示这个接口的参数是被加密的,进入方法之前,参数会自动被解密
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {
}
/**
* 接口方法上添加该注解,则返回的结果,会自动加密
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypt {
}
4.测试
@RestController
@RequestMapping("/secure")
@Slf4j
public class SecureController {
/**
* 参数加密测试,需要在方法上标注 @Decrypt 注解
*
* @param body
* @return
*/
// @Decrypt
@PostMapping("/decryptTest")
public List<String> decryptTest(@RequestBody List<String> body) {
log.info("参数加密测试,请求参数:{}", body);
return body;
}
// @Encrypt
@GetMapping("/encryptTest")
public List<String> encryptTest() {
List<String> encyptList = new ArrayList<>();
encyptList.add("1");
encyptList.add("2");
return encyptList;
}
}
返回密文
解析密文