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天

相关推荐
柏油3 小时前
MySQL InnoDB 行锁
数据库·后端·mysql
咖啡调调。3 小时前
使用Django框架表单
后端·python·django
白泽talk3 小时前
2个小时1w字| React & Golang 全栈微服务实战
前端·后端·微服务
摆烂工程师3 小时前
全网最详细的5分钟快速申请一个国际 “edu教育邮箱” 的保姆级教程!
前端·后端·程序员
一只叫煤球的猫4 小时前
你真的会用 return 吗?—— 11个值得借鉴的 return 写法
java·后端·代码规范
Asthenia04124 小时前
HTTP调用超时与重试问题分析
后端
颇有几分姿色4 小时前
Spring Boot 读取配置文件的几种方式
java·spring boot·后端
AntBlack4 小时前
别说了别说了 ,Trae 已经在不停优化迭代了
前端·人工智能·后端
@淡 定4 小时前
Spring Boot 的配置加载顺序
java·spring boot·后端