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. 服务端对应接口拿到解密后的数据,做后续的业务逻辑处理

系统交互流程

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

关键代码实现

前端加密示例

// 请求拦截器
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. 过滤器解密
@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. 自定义请求包装器(用于重复读取请求体以及替换请求体内容)
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:如何防止重放攻击?

// 服务端校验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
  • 统一算法参数:
    Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
    

Q4:RSA密钥对如何生成

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

扩展阅读

结语

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

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

项目源码:可在GitHub获取完整实现示例
安全建议:定期更换密钥对,长期密钥不超过90天

为了在Windows安装ADB工具,你可以按照以下步骤进行操作: 1. 首先,下载ADB工具包并解压缩到你自定义的安装目录。你可以选择将其解压缩到任何你喜欢的位置。 2. 打开运行窗口,可以通过按下Win+R键来快速打开。在运行窗口中输入"sysdm.cpl"并按下回车键。 3. 在系统属性窗口中,选择"高级"选项卡,然后点击"环境变量"按钮。 4. 在环境变量窗口中,选择"系统变量"部分,并找到名为"Path"的变量。点击"编辑"按钮。 5. 在编辑环境变量窗口中,点击"新建"按钮,并将ADB工具的安装路径添加到新建的路径中。确保路径正确无误后,点击"确定"按钮。 6. 返回到桌面,打开命令提示符窗口。你可以通过按下Win+R键,然后输入"cmd"并按下回车键来快速打开命令提示符窗口。 7. 在命令提示符窗口中,输入"adb version"命令来验证ADB工具是否成功安装。如果显示版本信息,则表示安装成功。 这样,你就成功在Windows安装ADB工具。你可以使用ADB工具来执行各种操作,如枚举设备、进入/退出ADB终端、文件传输、运行命令、查看系统日志等。具体的操作方法可以参考ADB工具的官方文档或其他相关教程。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [windows环境安装adb驱动](https://blog.csdn.net/zx54633089/article/details/128533343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Windows安装使用ADB简单易懂教程](https://blog.csdn.net/m0_37777700/article/details/129836351)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值