Ruoyi-Plus前后端分离场景下,数据加密传输

前后端分离场景下,数据加密传输

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 团队对于底层性能和工程解耦的考量:

  1. 绝对的最高优先级 :在 Filter 层面(Servlet 容器级)就把密文干掉,保证后续整个 Spring 链路(包括 Spring Security/Sa-Token 拦截、参数校验器 ValidatorUtils、日志切面等)拿到的全部都是干净的明文数据。如果做在 AOP 层,Sa-Token 等权限过滤器在 AOP 之前执行,根本拿不到明文参数。
  2. 极佳的解耦扩展性 :通过 ApiDecryptAutoConfiguration 实现随启随用。如果哪天项目不需要接口加密传输了,只需要在 application.yml 里将 api-decrypt.enabled 改为 false,这个最高优先级的 CryptoFilter 压根不会注入到 IoC 容器中,代码完全不产生任何性能或侵入式影响。
相关推荐
落魄大学生之流水线上谋生计2 小时前
Java锁全面指南:从基础概念到企业级应用
java·开发语言
why技术2 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端
学习星球2 小时前
Qwik 框架入门实战:从开源项目 Qwik City 开始,用可恢复性替代水合
后端·前端框架·开源·c5全栈
冬奇Lab2 小时前
开源项目第196期:LiveTalking — 实时交互流式数字人引擎,支持 Wav2Lip/MuseTalk/ER-NeRF
人工智能·开源·资讯
剑胆琴心静水深流3 小时前
全栈之路6---web集成与呈现
前端·vue.js·spring boot·分布式·spring·前端框架·npm
爱学习的小白柏3 小时前
【AI问数技术】多Agent协同架构:查询规划/SQL生成/洞察分析/报告生成
java·网络·人工智能·windows·sql·架构·llama
码匠许师傅4 小时前
【C++ 面试真题】30. 聊聊 C++ 的互斥锁与读写锁
java·c++·面试
沙盘客4 小时前
AFSIM 示例解读(03)· 六自由度飞行仿真 six_dof(下):机场运作与弹道导弹投送
java·javascript·经验分享
皮卡丘不断更4 小时前
从遥操作示范到可复现训练:LeRobot 0.6.1 的机器人学习工作流
人工智能·学习·机器人·开源·开发工具