前后端分离场景下,数据加密传输
Login接口信息加密传输
在 RuoYi-Vue-Plus v5.x 版本中,为了提高加解密的性能和安全性,系统全面升级为了 RSA + AES 混合加密机制 。在这种机制下,前端不需要 动态请求后端获取公钥,而是直接将后端固定的 RSA 公钥硬编码在前端配置中。下面整理了在 v5.6.2 版本中,前后端基于 RSA + AES 实现登录及所有请求接口信息加密传输的完整流程。
RSA + AES 混合加密整体流程图
这种模式的精髓在于:用 RSA 的非对称加密来安全传输 AES 密钥(解决密钥配送问题),用 AES 的对称加密来高效率加密业务数据(密码等)。
Spring MVC 控制层(Controller) Tomcat / Servlet 容器(CryptoFilter) Spring MVC 控制层(Controller) Tomcat / Servlet 容器(CryptoFilter) #mermaid-svg-MH0uXXkKRTBQP4VJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MH0uXXkKRTBQP4VJ .error-icon{fill:#552222;}#mermaid-svg-MH0uXXkKRTBQP4VJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MH0uXXkKRTBQP4VJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MH0uXXkKRTBQP4VJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MH0uXXkKRTBQP4VJ .marker.cross{stroke:#333333;}#mermaid-svg-MH0uXXkKRTBQP4VJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MH0uXXkKRTBQP4VJ p{margin:0;}#mermaid-svg-MH0uXXkKRTBQP4VJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MH0uXXkKRTBQP4VJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MH0uXXkKRTBQP4VJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-MH0uXXkKRTBQP4VJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-MH0uXXkKRTBQP4VJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-MH0uXXkKRTBQP4VJ .sequenceNumber{fill:white;}#mermaid-svg-MH0uXXkKRTBQP4VJ #sequencenumber{fill:#333;}#mermaid-svg-MH0uXXkKRTBQP4VJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-MH0uXXkKRTBQP4VJ .messageText{fill:#333;stroke:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MH0uXXkKRTBQP4VJ .labelText,#mermaid-svg-MH0uXXkKRTBQP4VJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .loopText,#mermaid-svg-MH0uXXkKRTBQP4VJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-MH0uXXkKRTBQP4VJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-MH0uXXkKRTBQP4VJ .noteText,#mermaid-svg-MH0uXXkKRTBQP4VJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-MH0uXXkKRTBQP4VJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MH0uXXkKRTBQP4VJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MH0uXXkKRTBQP4VJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-MH0uXXkKRTBQP4VJ .actorPopupMenu{position:absolute;}#mermaid-svg-MH0uXXkKRTBQP4VJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-MH0uXXkKRTBQP4VJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-MH0uXXkKRTBQP4VJ .actor-man circle,#mermaid-svg-MH0uXXkKRTBQP4VJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-MH0uXXkKRTBQP4VJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 随机生成 16位 AES 密钥 用固定 RSA 公钥加密 AES 密钥 用该 AES 密钥加密整个请求 JSON 5. 触发 CryptoFilter (最高优先级) 6. 提取 Header 中的 encrypt-key 7. RSA 私钥解密出明文 AES 密钥 8. 读取 Body,用 AES 密钥解密(还原出明文 JSON 字符串) 9. 通过 HttpServletRequestWrapper把还原后的明文包装成新的 Request 11. @PostMapping("/login")直接拿到纯净明文 body! 前端 Vue3 客户端 发送 POST /auth/login(Header: encrypt-key)110. 将包装后的明文 Request 放行传导2 前端 Vue3 客户端
前端 Vue3 实现流程与核心代码
在前端中,核心的拦截与加密逻辑封装在 src/utils/crypto.ts(加解密工具)和 src/utils/request.ts(Axios 拦截器)中。
1. 前端加解密工具类核心代码
前端使用 crypto-js 进行 AES 加密,使用 jsencrypt 进行 RSA 加密。此外,会生成随机的 16 位字符串作为 AES 密钥。
typescript
import CryptoJS from 'crypto-js'
import JSEncrypt from 'jsencrypt'
// 后端配置好的固定 RSA 公钥,通常配置在 .env.production 或全局常量中
const publicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...'
/**
* 随机生成 16 位的 AES 密钥
*/
export function generateAesKey() {
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
let aesKey = ''
for (let i = 0; i < 16; i++) {
aesKey += chars.charAt(Math.floor(Math.random() * chars.length))
}
return aesKey
}
/**
* RSA 加密方法(用于加密 AES 密钥)
*/
export function rsaEncrypt(data: string): string {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey)
return encryptor.encrypt(data) as string
}
/**
* AES 加密方法(用于加密真实的 Body 数据)
*/
export function aesEncrypt(data: string, aesKey: string): string {
const key = CryptoJS.enc.Utf8.parse(aesKey)
const encrypt = CryptoJS.AES.encrypt(data, key, {
mode: CryptoJS.mode.ECB, // 常用 ECB 或 CBC 模式
padding: CryptoJS.pad.Pkcs7
})
return encrypt.toString()
}
Axios 请求拦截器自动加密request.ts
在 v5.x 中,不仅是登录接口,所有配置了需要加密的接口(或全局开启加密时),都会在 Axios 请求拦截器中自动处理:
typescript
import axios from 'axios'
import { generateAesKey, rsaEncrypt, aesEncrypt } from '@/utils/crypto'
const service = axios.create({ ... })
service.interceptors.request.use(config => {
// 假设当前接口需要加密(例如登录接口,或者全局配置了 encrypt: true)
const isEncrypt = config.headers?.encrypt ?? true
if (isEncrypt && config.data) {
// 1. 随机生成本次请求的 AES 密钥
const aesKey = generateAesKey()
// 2. 使用硬编码的 RSA 公钥加密这个 AES 密钥
const encryptedAesKey = rsaEncrypt(aesKey)
// 3. 将加密后的 AES 密钥放入请求头中传递给后端
config.headers['encrypt-key'] = encryptedAesKey
// 4. 使用生成的明文 AES 密钥,对真实的请求体(包含密码的 Form 表单)进行 AES 加密
const dataStr = JSON.stringify(config.data)
config.data = {
// 将整个对象转为加密后的字符串发送
encryptData: aesEncrypt(dataStr, aesKey)
}
}
return config
}, error => {
return Promise.reject(error)
})
后端 Spring Boot 实现流程与核心代码
在 RuoYi-Vue-Plus v5.6.2 中,若依既没有在 Spring MVC 层(Advice)做手脚,也没有在 AOP 层(Aspect)做拦截,而是把解密逻辑直接做在了 Servlet 规范中最顶层的全局过滤器(Filter) 中,并且赋予了它 最高优先级(HIGHEST_PRECEDENCE)。
这意味着,请求甚至还没有进入 Spring 容器、还没有匹配到任何 Controller 时,就已经在过滤器层被强制解密并重写了。
后端属性配置application.yml
后端拥有和前端配对的 RSA 私钥。
yaml
# api接口加密
api-decrypt:
# 是否开启全局接口加密
enabled: true
# AES 加密头标识
headerFlag: encrypt-key
# 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
# 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
# 请求解密私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
# 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
后端核心-全局解密过滤器CryptoFilter
解密逻辑直接做在了 Servlet 规范中最顶层的全局过滤器(Filter) 中,并且赋予了它 最高优先级(HIGHEST_PRECEDENCE)。
java
@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class)
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
public class ApiDecryptAutoConfiguration {
@Bean
@FilterRegistration(
name = "cryptoFilter",
urlPatterns = "/*",
order = FilterRegistrationBean.HIGHEST_PRECEDENCE,
dispatcherTypes = DispatcherType.REQUEST
)
public CryptoFilter cryptoFilter(ApiDecryptProperties properties) {
return new CryptoFilter(properties);
}
}
java
/**
* Crypto 过滤器
*
* @author wdhcr
*/
public class CryptoFilter implements Filter {
private final ApiDecryptProperties properties;
public CryptoFilter(ApiDecryptProperties properties) {
this.properties = properties;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
// 获取加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
boolean responseFlag = apiEncrypt != null && apiEncrypt.response();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
EncryptResponseBodyWrapper responseBodyWrapper = null;
// 是否为 put 或者 post 请求
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
// 是否存在加密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
if (StringUtils.isNotBlank(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
} else {
// 是否有注解,有就报错,没有放行
if (ObjectUtil.isNotNull(apiEncrypt)) {
HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
exceptionResolver.resolveException(
servletRequest, servletResponse, null,
new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
return;
}
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
chain.doFilter(
ObjectUtil.defaultIfNull(requestWrapper, request),
ObjectUtil.defaultIfNull(responseWrapper, response));
if (responseFlag) {
servletResponse.reset();
// 对原始内容加密
String encryptContent = responseBodyWrapper.getEncryptContent(
servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
// 对加密后的内容写出
servletResponse.getWriter().write(encryptContent);
}
}
/**
* 获取 ApiEncrypt 注解
*/
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
// 获取注解
try {
HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
if (ObjectUtil.isNotNull(mappingHandler)) {
Object handler = mappingHandler.getHandler();
if (ObjectUtil.isNotNull(handler)) {
// 从handler获取注解
if (handler instanceof HandlerMethod handlerMethod) {
return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
}
}
}
} catch (Exception e) {
return null;
}
return null;
}
@Override
public void destroy() {
}
}
控制层无感接收
得益于上面的 CryptoFilter 拦截解密,在 AuthController 中,代码不需要做任何特殊处理,直接拿到的 loginBody.getPassword() 已经是明文密码了。
java
/**
* 登录方法
*
* @param body 登录信息
* @return 结果
*/
@ApiEncrypt
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
ValidatorUtils.validate(loginBody);
// 授权类型和客户端id
String clientId = loginBody.getClientId();
String grantType = loginBody.getGrantType();
SysClientVo client = clientService.queryByClientId(clientId);
// 查询不到 client 或 client 内不包含 grantType
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
return R.fail(MessageUtils.message("auth.grant.type.error"));
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
}
// 校验租户
loginService.checkTenant(loginBody.getTenantId());
// 登录
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
SseMessageDto dto = new SseMessageDto();
dto.setMessage(DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Vue-Plus 后台管理系统");
dto.setUserIds(List.of(userId));
SseMessageUtils.publishMessage(dto);
}, 5, TimeUnit.SECONDS);
return R.ok(loginVo);
}
这种设计的优势分析
看完了真正的源码架构,不得不佩服 RuoYi-Vue-Plus 团队对于底层性能和工程解耦的考量:
- 绝对的最高优先级 :在 Filter 层面(Servlet 容器级)就把密文干掉,保证后续整个 Spring 链路(包括 Spring Security/Sa-Token 拦截、参数校验器
ValidatorUtils、日志切面等)拿到的全部都是干净的明文数据。如果做在 AOP 层,Sa-Token 等权限过滤器在 AOP 之前执行,根本拿不到明文参数。 - 极佳的解耦扩展性 :通过
ApiDecryptAutoConfiguration实现随启随用。如果哪天项目不需要接口加密传输了,只需要在application.yml里将api-decrypt.enabled改为false,这个最高优先级的CryptoFilter压根不会注入到 IoC 容器中,代码完全不产生任何性能或侵入式影响。