1. 背景
近期项目需要对接三方平台,双方约定HTTP请求的RequestBody需要使用对称加密方法进行加密,这就导致需要对部分接口进行统一的解密处理,避免冗余的校验和解密工作。
2. 实现
2.1 解密注解
创建解密注解,以便灵活添加在需要使用解密的接口。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptRequest {
boolean value() default true;
}
2.2 请求拦截器
创建请求拦截器,对加密接口依次进行校验、解密操作。
@Slf4j
@ControllerAdvice
public class EncryptRequestBodyAdvice extends RequestBodyAdviceAdapter {
private static final String TIMESTAMP = "timestamp";
private static final String NONCE = "nonce";
private static final String SIGN = "sign";
private static final String ACCESS_TOKEN = "accessToken";
private static final String APP_ID = "appId";
private static final String UTF_8 = "utf-8";
// 请求体加密配置项
private final AesConfig aesConfig;
private final RedissonClient redissonClient;
private static final Cache<MethodParameter, Boolean> cache = CacheBuilder.newBuilder()
.maximumSize(128).expireAfterAccess(10, TimeUnit.MINUTES).build();
public EncryptRequestBodyAdvice(AesConfig aesConfig, RedissonClient redissonClient) {
this.aesConfig = aesConfig;
this.redissonClient = redissonClient;
}
// 使用缓存加速注解判断
private boolean hasEncryptRequestAnnotation(MethodParameter methodParameter) {
try {
return cache.get(methodParameter,
() -> methodParameter.hasMethodAnnotation(EncryptRequest.class));
} catch (Exception e) {
log.error("检查{}是否包含EncryptRequest注解失败", methodParameter.getMethod(), e);
}
return false;
}
// 重载supports函数,查看当前业务接口是否需要进行解密操作
@Override
public boolean supports(@NotNull MethodParameter methodParameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return hasEncryptRequestAnnotation(methodParameter);
}
@NotNull
@Override
public HttpInputMessage beforeBodyRead(@NotNull HttpInputMessage inputMessage,
@NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType)
throws IOException {
HttpHeaders headers = inputMessage.getHeaders();
String timestamp, nonce, appId, signature, accessToken;
try {
timestamp = String.valueOf(
Objects.requireNonNull(headers.get(TIMESTAMP), "timestamp不能为空").get(0));
nonce = String.valueOf(
Objects.requireNonNull(headers.get(NONCE), "nonce不能为空").get(0));
appId = String.valueOf(
Objects.requireNonNull(headers.get(APP_ID), "appId不能为空").get(0));
signature = String.valueOf(
Objects.requireNonNull(headers.get(SIGN), "sign不能为空").get(0));
accessToken = String.valueOf(
Objects.requireNonNull(headers.get(ACCESS_TOKEN), "accessToken不能为空").get(0));
} catch (Exception e) {
log.error("Signature verification failed, request header param is null", e);
throw new BusinessException(ResponseStatus.SIGNATURE_FAILED);
}
// 检查header参数是否满足要求
validateHeaderValues(timestamp, appId, signature, nonce, accessToken);
// 检查时间戳
validateTimestamp(timestamp);
// 检查对接平台ID
validateAppId(appId);
// 检查对接平台的噪声
validateNonce(appId, nonce);
// 检查访问Token是否有效
validateAccessToken(appId, accessToken);
Map<String, String> headerParams = new HashMap<>(16);
headerParams.put(NONCE, nonce);
headerParams.put(TIMESTAMP, timestamp);
headerParams.put(APP_ID, appId);
headerParams.put(ACCESS_TOKEN, accessToken);
String decryptBody = decryptRequestBody(inputMessage);
validateSignature(signature, headerParams, decryptBody);
// 解密后的请求体内容,需要重新序列化后存储至请求中,以便后续接口层读取解密后的请求体
return new DecryptInputStream(new ByteArrayInputStream(decryptBody.getBytes()), headers);
}
private void validateHeaderValues(String timestamp, String appId, String signature,
String nonce, String accessToken) {
// 实现省略
}
private void validateTimestamp(String timestamp) {
// 实现省略
}
private void validateAppId(String appId) {
// 实现省略
}
private void validateNonce(String appId, String nonce) {
// 实现省略
}
private void validateAccessToken(String appId, String accessToken) {
// 实现省略
}
// 解密请求体
private String decryptRequestBody(HttpInputMessage inputMessage) {
try {
return AesUtil.decrypt(
// 读取加密的请求体内容,并根据加密参数进行解密
StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8),
aesConfig.getAppSecretKey(), aesConfig.getAppSecretIV());
} catch (Exception e) {
log.error("开放平台接口解密请求体失败", e);
throw new BusinessException(ResponseStatus.SIGNATURE_FAILED);
}
}
private void validateSignature(String reqSignature, Map<String, String> headerParams,
String decryptBody) {
// 实现省略
}
// 创建解密的输入流,以存储解密的RequestBody
public static class DecryptInputStream implements HttpInputMessage {
private final InputStream body;
private final HttpHeaders headers;
public DecryptInputStream(InputStream body, HttpHeaders headers) {
this.body = body;
this.headers = headers;
}
@NotNull
@Override
public InputStream getBody() {
return body;
}
@NotNull
@Override
public HttpHeaders getHeaders() {
return headers;
}
}
}