2团
Published on 2025-03-06 / 5 Visits
0
0

Spring Boot2解密HTTP加密请求体

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;
        }
    }
}


Comment