SpringBoot 注解式接口加解密

419 阅读6分钟

一、架构设计

  1. 注解驱动 - 使用 @Crypto标记需要加解密的接口
  2. 配置中心 - 支持动态开关和算法选择
  3. 算法扩展 - 可插拔的加密算法模块
  4. 消息处理 - 使用Spring的Advice机制处理请求/响应

二、代码实现

2.1 加解密注解定义

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Crypto {
    /**
     * 是否加密响应(默认true)
     */
    boolean response() default true;

    /**
     * 是否解密请求(默认true)
     */
    boolean request() default true;
}

2.2 加解密配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "crypto")
@Data
public class CryptoProperties {
    /**
     * 是否启用加解密(默认关闭)
     */
    private boolean enabled = false;
    
    /**
     * 加密算法(默认AES)
     */
    private String algorithm = "aes";
    
    /**
     * AES密钥(BASE64格式)
     */
    private String aesKey;
}

2.3 加解密处理器接口

希望可以灵活替换,所以应该定义一个接口,比如CryptoProcessor,然后有不同的实现类,比如AESCryptoProcessor。这样以后要换算法的话,只需要换实现类,或者通过配置选择不同的实现

import cn.hutool.crypto.CryptoException;

public interface CryptoProcessor {
    /**
     * 加密方法
     *
     * @param content 待加密内容
     * @return 加密后的内容
     * @throws CryptoException 加解密异常
     */
    String encrypt(String content) throws CryptoException;

    /**
     * 解密方法
     *
     * @param content 待解密内容
     * @return 解密后的内容
     * @throws CryptoException 加解密异常
     */
    String decrypt(String content) throws CryptoException;
}

2.4 加解密处理器接口实现类

import cn.hutool.crypto.CryptoException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;

public class AESCryptoProcessor implements CryptoProcessor {
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
    private final SecretKey secretKey;
    private final IvParameterSpec iv;

    public AESCryptoProcessor(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        this.secretKey = new SecretKeySpec(keyBytes, "AES");
        this.iv = new IvParameterSpec(Arrays.copyOfRange(keyBytes, 0, 16));
    }

    @Override
    public String encrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
            byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new CryptoException("AES加密失败", e);
        }
    }

    @Override
    public String decrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
            byte[] decoded = Base64.getDecoder().decode(content);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new CryptoException("AES解密失败", e);
        }
    }
}

2.5 请求处理Advice

RequestBodyAdvice可以在读取请求体之前进行解密

import cn.hutool.crypto.CryptoException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CryptoRequestBodyAdvice implements RequestBodyAdvice {

    private final CryptoProcessor cryptoProcessor;
    private final CryptoProperties properties;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public CryptoRequestBodyAdvice(CryptoProcessor cryptoProcessor, CryptoProperties properties) {
        this.cryptoProcessor = cryptoProcessor;
        this.properties = properties;
    }

    @Override
    public boolean supports(MethodParameter parameter, Type targetType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return properties.isEnabled() 
            && parameter.hasMethodAnnotation(Crypto.class)
            && Objects.requireNonNull(parameter.getMethodAnnotation(Crypto.class)).request();
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
                                           MethodParameter parameter,
                                           Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        // 如果启用加密
        if(properties.isEnabled()) {
            // 1. 读取加密数据
            String encrypted = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
            // 像这样的字符串:""abc"", 变成 "abc"
            encrypted = encrypted.replaceAll("^"|"$", "");
            // 2. 执行解密
            String decrypted = cryptoProcessor.decrypt(encrypted);
            // 3. 验证数据有效性
            validateDecryptedData(decrypted, targetType);

            return new ByteArrayHttpInputMessage(
                    decrypted.getBytes(StandardCharsets.UTF_8),
                    inputMessage.getHeaders()
            );
        }
        return inputMessage;
    }
    private void validateDecryptedData(String decrypted, Type targetType) {
        try {
            // 使用Spring的类型转换系统验证
            new ObjectMapper().readValue(decrypted, constructJavaType(targetType));
        } catch (IOException e) {
            throw new CryptoException("解密数据格式无效: " + e.getMessage(), e);
        }
    }

    private JavaType constructJavaType(Type targetType) {
        return TypeFactory.defaultInstance().constructType(targetType);
    }


    public static class ByteArrayHttpInputMessage implements HttpInputMessage {
        private final byte[] body;
        private final HttpHeaders headers;

        public ByteArrayHttpInputMessage(byte[] body, HttpHeaders headers) {
            this.body = body;
            // 必须复制原始headers防止污染
            this.headers = new HttpHeaders();
            this.headers.putAll(headers);
            // 显式设置内容长度
            this.headers.setContentLength(body.length);
        }

        @Override
        public InputStream getBody() throws IOException {
            return new ByteArrayInputStream(body);
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    private Object parseDecryptedData(String decrypted, Type targetType) throws JsonProcessingException {
        return objectMapper.readValue(decrypted, objectMapper.constructType(targetType));
    }
}

2.6 响应处理Advice

ResponseBodyAdvice可以在返回响应体之前进行加密

import cn.bdmcom.common.web.Result;
import cn.hutool.crypto.CryptoException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Objects;

@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class CryptoResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final CryptoProcessor cryptoProcessor;
    private final CryptoProperties properties;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public CryptoResponseBodyAdvice(CryptoProcessor cryptoProcessor, CryptoProperties properties) {
        this.cryptoProcessor = cryptoProcessor;
        this.properties = properties;
    }

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return (properties.isEnabled() && ((returnType.hasMethodAnnotation(Crypto.class) && Objects.requireNonNull(returnType.getMethodAnnotation(Crypto.class)).response()))
                || isExceptionResponse(returnType)
        );
    }

    private boolean isExceptionResponse(MethodParameter returnType) {
        return Objects.requireNonNull(returnType.getMethod()).getDeclaringClass()
                .isAnnotationPresent(ResponseBody.class);
    }


    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

        // 如果启动加密
        try {
            if (body instanceof Result<?> result && properties.isEnabled()) {
                // 添加请求头
                response.getHeaders().add("x-encrypt-response", "AES");
                response.getHeaders().add("x-encrypt-error", "AES");
                String rawData = objectMapper.writeValueAsString(result);
                return cryptoProcessor.encrypt(rawData);
            }
        } catch (Exception e) {
            throw new CryptoException("异常响应加密失败", e);
        }
        return body;
    }
}

2.7 自动配置类

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(CryptoProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CryptoAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public CryptoProcessor cryptoProcessor(CryptoProperties properties) {
        if ("aes".equalsIgnoreCase(properties.getAlgorithm())) {
            return new AESCryptoProcessor(properties.getAesKey());
        }
        throw new UnsupportedOperationException("不支持的加密算法: " + properties.getAlgorithm());
    }

    @Bean
    @ConditionalOnBean(CryptoProcessor.class)
    public CryptoRequestBodyAdvice cryptoRequestBodyAdvice(CryptoProcessor processor,
                                                         CryptoProperties properties) {
        return new CryptoRequestBodyAdvice(processor, properties);
    }

    @Bean
    @ConditionalOnBean(CryptoProcessor.class)
    public CryptoResponseBodyAdvice cryptoResponseBodyAdvice(CryptoProcessor processor,
                                                            CryptoProperties properties) {
        return new CryptoResponseBodyAdvice(processor, properties);
    }
}

2.8 配置文件

crypto:
  enabled: true # 是否开启
  algorithm: aes
  aes-key: "MTIzNDU2Nzg5MDEyMzQ1Ng==" # BASE64格式的16/24/32字节密钥

三、验证方法测试

3.1 测试接口

@Tag(name = "管理员管理控制器")
@RestController
@RequestMapping("/auth/admin")
public class UserController extends BaseController {
    @Resource
    private IUserService userService;

    @PostMapping("/login")
    @Operation(summary = "管理员登录")
    @Crypto
    public Result<String> login(@RequestBody UserDto userInfo) {
        return setOk(("登录成功"), userService.login(userInfo));
    }
}

3.2 测试Service

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDo> implements IUserService {
    /**
     * 登录
     *
     * @param userInfo 用户信息
     * @return token
     */
    @Override
    @SneakyThrows
    public String login(UserDto userInfo) {
        if ("admin".equals(userInfo.getUsername()) && "123456".equals(userInfo.getPassword())) {
            StpUtil.login((546));
            return StpUtil.getTokenValue();
        }
        throw new Exception("用户名或密码错误");
    }
}

3.3 单元测试

@SpringBootTest
class AESCryptoProcessorTest {
    
    private AESCryptoProcessor aesCryptoProcessor = new AESCryptoProcessor("MTIzNDU2Nzg5MDEyMzQ1Ng==");

    @Test
    void encrypt() {
        String encrypt = aesCryptoProcessor.encrypt("{\n" +
                "  "username": "admin",\n" +
                "  "password": "123456",\n" +
                "  "role": ""\n" +
                "}");
        System.out.println(encrypt);
    }
    
    @Test
    void decrypt() {
        String decrypt = aesCryptoProcessor.decrypt("6tmr623112r3OFkPes2XetsKcVO/FgDE+UD3cT8dxBWC8c8gKMNcqAhxPORuk3W581r0KKbDY/O4iOWX79HQN2hqv/LDMdlTEhJml4zZUd5qtomeU51eaHauujsg5FoGsn7/Hgq2vHze2q5JuQbQUw==");
        System.out.println(decrypt);
    }
}

3.4 通过单元测试将参数进行加密,得到入参

w4JeMik62JKILEZAulgav0rFDr+J2HegTk+2XxQgMXF4UrmiYPtLg5sd5pwPYqnXrskGhjC0VrFaVR1xFQz3dA==

3.5 请求接口,得到解密后的返回值

6tmr623112r3OFkPes2XetsKcVO/FgDE+UD3cT8dxBWC8c8gKMNcqAhxPORuk3W581r0KKbDY/O4iOWX79HQN2hqv/LDMdlTEhJml4zZUd5qtomeU51eaHauujsg5FoGsn7/Hgq2vHze2q5JuQbQUw==

3.6 对返回值进行解密,得到解密信息

四、前后端联调

4.1 加解密工具类封装

安装依赖

pnpm install crypto-js qs

/src/utils/crypto.js

import CryptoJS from 'crypto-js'

const CRYPTO_CONFIG = {
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
}

export const aes = {
    encrypt(data, key) {
        const keyBytes = CryptoJS.enc.Base64.parse(key)
        const iv = CryptoJS.lib.WordArray.create(
            keyBytes.words.slice(0, 4) // 取前16字节
        )

        const encrypted = CryptoJS.AES.encrypt(
            JSON.stringify(data),
            keyBytes,
            { ...CRYPTO_CONFIG, iv }
        )
        return encrypted.toString()
    },

    decrypt(ciphertext, key) {
        const keyBytes = CryptoJS.enc.Base64.parse(key)
        const iv = CryptoJS.lib.WordArray.create(
            keyBytes.words.slice(0, 4))

        const decrypted = CryptoJS.AES.decrypt(
            ciphertext,
            keyBytes,
            { ...CRYPTO_CONFIG, iv }
        )
        return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
    }
}

4.2 axios封装

src/api/request.js

import axios from 'axios'
import { aes } from '../utils/crypto'
import qs from 'qs'

const service = axios.create({
    baseURL: import.meta.env.VUE_APP_API_BASE_URL,
    timeout: 10000,
    paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
})

// 请求加密拦截器
service.interceptors.request.use(config => {
    debugger
    if (config.crypto) {
        const key = import.meta.env.VITE_VUE_APP_AES_KEY
        config.data = aes.encrypt(config.data, key)
        // 添加加密标识头
        config.headers['x-encrypt-request'] = 'AES'
        config.headers['Content-Type'] = 'application/json'
    }
    return config
})

// 响应解密拦截器
service.interceptors.response.use(response => {
    if (response.headers['x-encrypt-response']) {
        const key = import.meta.env.VITE_VUE_APP_AES_KEY
        response.data = aes.decrypt(response.data, key)
    }
    return response.data
}, error => {
    // 处理加密错误响应
    if (error.response?.headers['x-encrypt-error']) {
        const key = import.meta.env.VITE_VUE_APP_AES_KEY
        const decryptedError = aes.decrypt(error.response.data, key)
        error.message = decryptedError.msg
    }
    console.log('请求失败:', error)
    return Promise.reject(error)
})

export default service

4.3 测试接口

/src/api/userInfoRequest.js

import request from "./request.js";
export function postSecureData(data) {
    return request({
        url: '/api/auth/admin/login',
        method: 'post',
        data: data,
        crypto: true // 加密请求
    })
}

4.4 Vue3页面测试

前端使用技术栈:vue3.5.13、axios1.9.0、ant-design-vue4.x、crypto-js4.2.0

<script setup>
import {postSecureData} from './api/userInfoRequest.js'
import {reactive} from "vue";
import {aes} from './utils/crypto.js'
import {message} from "ant-design-vue";

const user = reactive({
  username:'admin',
  password:'123456'
})

const login = async () => {
  console.log("点击成功")
  const res = await postSecureData(user)
  if (res.code === 200 && res.success) {
    message.info('登录成功')
  } else {
    message.error(res.msg)
  }

}
console.log(aes.encrypt(user,  'MTIzNDU2Nzg5MDEyMzQ1Ng=='))
console.log(aes.decrypt('6tmr623112r3OFkPes2XetsKcVO/FgDE+UD3cT8dxBWC8c8gKMNcqAhxPORuk3W52YPDDP0XKLacMp1Jk8h5MUDOsYKzVo6BaH6Y2aUdggJCu5+j/q65nMDMXTY5qPWwOFSYGIr4Hz9tm6VEwAXEAQ==',  'MTIzNDU2Nzg5MDEyMzQ1Ng=='))
</script>

<template>
  <a-input v-model:value="user.username" placeholder="请输入用户名"/>
  <a-input v-model:value="user.password" placeholder="请输入密码"/>
  <a-button type="primary" @click="login">请求接口</a-button>
</template>

<style scoped>

</style>

4.5 请求参数和返回值

请求参数加密

请求返回值加密

4.6 页面响应结果

登录成功,正常显示

登录失败,正常显示