一、架构设计
- 注解驱动 - 使用
@Crypto
标记需要加解密的接口 - 配置中心 - 支持动态开关和算法选择
- 算法扩展 - 可插拔的加密算法模块
- 消息处理 - 使用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 页面响应结果
登录成功,正常显示
登录失败,正常显示