AES+RSA实现前后端加密通信:全方位安全解决方案

引言

最近在项目上遇到了前后端通信时需要对数据进行加密的需求,网上搜罗了一大堆方案,大多介绍的都不太全,不能直接应用到项目中,所以就借此机会出一个完整的前后端数据通信加解密处理方案。

为什么需要接口加密

在现代Web应用中,数据安全传输面临三大核心挑战:

  1. 防窃听:防止敏感数据在传输过程中被第三方截获
  2. 防篡改:确保数据在传输过程中不被恶意修改
  3. 防重放:避免请求被截获后重复发送

单纯使用HTTPS并不能解决所有问题,特别是当传输包含用户隐私、支付信息等敏感数据时,这种风险更加不可接受。接口加密能够确保即使数据被截获,攻击者也无法理解其中的内容。

混合加密的技术选型

  1. 算法特性对比
算法类型 代表算法 特点 适用场景
对称加密 AES 加密速度快,但密钥分发难 大数据量业务数据加密
非对称加密 RSA 安全性高,但加密速度较慢 密钥交换与数据签名
  1. 混合加密的优势
  • 性能平衡:AES处理业务数据,RSA保护AES密钥
  • 完美前向保密:每次会话生成独立AES密钥
  • 密钥管理简化:服务端只需保管RSA私钥

实现原理

  1. 客户端与服务端先约定并保存好RSA密钥对
  2. 客户端随机生成AES密钥,使用RSA公钥加密这个密钥
  3. 使用AES密钥加密请求体数据
  4. 将原始的请求体数据与时间戳、随机字符串、AES密钥一起生成签名
  5. 将加密后的AES密钥和加密后的数据以及时间戳、随机字符串等参数一起发送给服务端
  6. 服务端接收到请求后,先用RSA私钥解密得到AES密钥,再用AES密钥解密数据,然后再对数据做签名校验
  7. 服务端对应接口拿到解密后的数据,做后续的业务逻辑处理

系统交互流程

sequenceDiagram participant 前端 participant 过滤器 participant 后端服务 前端->>过滤器: POST /userData/submit-form Note Over 前端, 过滤器: 请求头: Note Over 前端, 过滤器: - timestamp: 时间戳 Note Over 前端, 过滤器: - nonce: 随机数 Note Over 前端, 过滤器: - signature: 签名 Note Over 前端, 过滤器: - encrypted-key: RSA加密后的AES密钥 Note Over 前端, 过滤器: 请求体: Note Over 前端, 过滤器: { data: AES加密的业务数据 } 过滤器->>过滤器: 1. 验证时间戳有效性(±5分钟) 过滤器->>过滤器: 2. 检查nonce是否重复(Redis防重放) 过滤器->>过滤器: 3. RSA解密获取AES密钥 过滤器->>过滤器: 4. AES解密请求体数据 过滤器->>过滤器: 5. 验证签名(SHA256WithRSA) 过滤器->>后端服务: 6. 明文请求体(JSON格式) 后端服务->>过滤器: 7. 明文响应数据 过滤器->>过滤器: 8. AES加密响应 过滤器->>前端: 9. 返回加密数据(HTTP 200) Note Over 过滤器, 后端服务: 响应头: Note Over 过滤器, 后端服务: - encrypted-key: RSA加密的AES密钥

关键代码实现

前端加密示例

javascript 复制代码
// 请求拦截器
service.interceptors.request.use(
	config => {
		const timestamp = Date.now().toString();
		const nonce = crypto.generateNonce();

		// 判断是否为form-data请求
		const isFormData = config.headers['Content-Type']?.includes('multipart/form-data');
		if (!isFormData) {
			// 生成AES密钥与随机向量
			const aesKey = crypto.generateAESKey();
			const iv = crypto.generateIV();

			// RSA公钥加密AES密钥
			const keyData = JSON.stringify({ key: aesKey, iv: iv });
			const encryptedAESKey = crypto.rsaEncrypt(keyData);

			// AES密钥加密请求体
			const originalData = config.data || {};
			const encryptedData = crypto.aesEncrypt(originalData, aesKey, iv);

			// 生成签名
			const signature = crypto.generateSignature(originalData, timestamp, nonce, 
			
			// 添加请求头
			config.headers['X-Encrypted-Key'] = encryptedAESKey;
			config.headers['X-Signature'] = signature;
			config.headers['X-Timestamp'] = timestamp;
			config.headers['X-Nonce'] = nonce;

			config.data = {
				data: encryptedData,
			};
		}

		return config;
	},
	error => {
		console.error('请求错误:', error);
		return Promise.reject(error);
	}
);

后端解密示例

  1. 过滤器解密
java 复制代码
@Component
public class DecryptionFilter implements Filter {

    private final Logger log = LoggerFactory.getLogger(DecryptionFilter.class);

    private final SecurityService securityService;

    private final RSAUtils rsaUtils;

    public DecryptionFilter(SecurityService securityService, RSAUtils rsaUtils) {
        this.securityService = securityService;
        this.rsaUtils = rsaUtils;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (!requiresDecryption(httpRequest)) {
            chain.doFilter(request, response);
            return;
        }

        String encryptedKey = httpRequest.getHeader(CommonConstants.ENCRYPTED_KEY);
        String signature = httpRequest.getHeader(CommonConstants.SIGNATURE);
        String timestamp = httpRequest.getHeader(CommonConstants.TIMESTAMP);
        String nonce = httpRequest.getHeader(CommonConstants.NONCE);

		// 参数完整性校验
        if (StringUtils.isAnyBlank(encryptedKey, signature, timestamp, nonce)) {
            sendErrorResponse(httpResponse, "请求头参数缺失", HttpStatus.BAD_REQUEST);
            return;
        }

        try {
        	// 时间戳校验(5分钟内)
            if (!securityService.validateTimestamp(Long.parseLong(timestamp))) {
                sendErrorResponse(httpResponse, "请求已过期", HttpStatus.BAD_REQUEST);
                return;
            }

			// 重复请求校验
            if (!securityService.validateNonce(nonce)) {
                sendErrorResponse(httpResponse, "重复的请求", HttpStatus.BAD_REQUEST);
                return;
            }

            HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);
            Map<String, Object> requestBodyMap = JSON.parseObject(requestWrapper.getBody(), Map.class);
            String encryptedData = (String) requestBodyMap.get("data");

            if (StringUtils.isBlank(encryptedData)) {
                chain.doFilter(request, response);
            }

			// RSA私钥解密AES密钥
            String aesKeyJson = rsaUtils.decryptByBlock(encryptedKey);
            AESKey aesKey = JSON.parseObject(aesKeyJson, AESKey.class);

			// 解密请求数据
            String decryptedData;
            try {
                log.info("获取到加密数据: \n{}", encryptedData);
                decryptedData = AESUtils.decrypt(encryptedData, aesKey);
                log.info("解密后的数据: \n{}", decryptedData);
            } catch (Exception e) {
                log.error("数据解密失败: {}", e.getMessage(), e);
                sendErrorResponse(httpResponse, "内部服务器错误", HttpStatus.INTERNAL_SERVER_ERROR);
                return;
            }

            Map<String, Object> params = JSON.parseObject(decryptedData, Map.class);

			// 数据签名验证
            if (!securityService.verifySignature(params, timestamp, nonce, aesKey.getKey(), signature)) {
                sendErrorResponse(httpResponse, "数据签名验证失败", HttpStatus.BAD_REQUEST);
                return;
            }
            log.info("数据签名验证通过!");

            requestWrapper.setBody(decryptedData);

            chain.doFilter(requestWrapper, response);
        } catch (Exception e) {
            sendErrorResponse(httpResponse, "数据解密失败", HttpStatus.BAD_REQUEST);
        }

    }

    /**
     * 判断请求是否需要解密处理
     */
    private boolean requiresDecryption(HttpServletRequest request) {
        // 1. 只处理POST/PUT/PATCH请求
        String method = request.getMethod().toUpperCase();
        if (!"POST".equals(method) && !"PUT".equals(method) && !"PATCH".equals(method)) {
            return false;
        }

        // 2. 检查Content-Type
        String contentType = request.getContentType();
        if (contentType == null || !contentType.toLowerCase().contains(MediaType.APPLICATION_JSON_VALUE)) {
            return false;
        }

        // 3. 只处理特定路径的请求
        String uri = request.getRequestURI();
        return uri.startsWith("/userData");
    }

    private void sendErrorResponse(HttpServletResponse response, String message, HttpStatus status)
            throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(
                String.format("{\"msg\":\"%s\",\"code\":%d}", message, status.value())
        );
    }
}
  1. 自定义请求包装器 (用于重复读取请求体以及替换请求体内容)
java 复制代码
public class HttpRequestWrapper extends HttpServletRequestWrapper {

    private byte[] body;

    public HttpRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        if (request.getContentLength() > 0) {
            this.body = readBytes(request.getReader());
        } else {
            this.body = new byte[0];
        }
    }

    public HttpRequestWrapper(HttpServletRequest request, String body) {
        super(request);
        this.body = body.getBytes();
    }

    public String getBody() {
        return new String(body);
    }

    public void setBody(String body) {
        this.body = body.getBytes();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new ByteArrayServletInputStream(body);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    private byte[] readBytes(BufferedReader reader) throws IOException {
        StringBuilder sb = new StringBuilder();
        char[] buffer = new char[1024];
        int bytesRead;
        while ((bytesRead = reader.read(buffer)) != -1) {
            sb.append(buffer, 0, bytesRead);
        }
        return sb.toString().getBytes();
    }

    private static class ByteArrayServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream buffer;

        public ByteArrayServletInputStream(byte[] body) {
            this.buffer = new ByteArrayInputStream(body);
        }

        @Override
        public int read() {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    }
}

注意: 以上代码示例中部分代码可根据实际需求自行调整

如只处理特定请求uri.startsWith("/userData");这里只做示例验证,跟实际业务无关。

本文涉及到的技术组件

  • 前端加密库:crypto-js
  • Java安全库:Bouncy Castle

常见问题

Q1:如何防止重放攻击?

java 复制代码
// 服务端校验nonce
public boolean validateNonce(String nonce) {
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
            "nonce:" + nonce,
            "1",
            timestampTolerance,
            TimeUnit.MINUTES
    );
    return success != null && success;
}

Q2:为什么选择过滤器解密?

  • 统一处理:集中所有加密接口的安全逻辑
  • 业务无感知:控制器无需关心解密细节
  • 提前拦截:在进入Spring MVC前完成验证
  • 性能可控:可针对加密操作单独优化

Q2:iOS/Android如何兼容?

  • 使用跨平台加密库:React Native用react-native-crypto-js

  • 统一算法参数:

    java 复制代码
    Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")

Q4:RSA密钥对如何生成

可通过在线生成工具生成,生成后注意保存好密钥对。

扩展阅读

结语

AES+RSA混合加密方案在保证安全性的同时兼顾了系统性能,是当前Web应用加密通信的最佳实践。本文提供的实现方案具有以下特点:

  1. 开箱即用:完整代码片段可直接集成
  2. 灵活扩展:支持算法动态切换
  3. 全栈覆盖:包含前后端完整实现

项目源码:可在GitHub获取完整实现示例

安全建议:定期更换密钥对,长期密钥不超过90天

相关推荐
头孢头孢1 小时前
k8s常用总结
运维·后端·k8s
TheITSea1 小时前
后端开发 SpringBoot 工程模板
spring boot·后端
Asthenia04121 小时前
编译原理中的词法分析器:从文本到符号的桥梁
后端
Asthenia04121 小时前
用RocketMQ和MyBatis实现下单-减库存-扣钱的事务一致性
后端
Pasregret2 小时前
04-深入解析 Spring 事务管理原理及源码
java·数据库·后端·spring·oracle
Micro麦可乐2 小时前
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
java·spring boot·后端·spring·intellij-idea·spring security
returnShitBoy2 小时前
Go语言中的defer关键字有什么作用?
开发语言·后端·golang
Asthenia04122 小时前
面试场景题:基于Redisson、RocketMQ和MyBatis的定时短信发送实现
后端
Asthenia04122 小时前
链路追踪视角:MyBatis-Plus 如何基于 MyBatis 封装 BaseMapper
后端
Ai 编码助手2 小时前
基于 Swoole 的高性能 RPC 解决方案
后端·rpc·swoole