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

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


目录

  • 引言
  • 为什么需要接口加密
  • 混合加密的技术选型
  • 实现原理
  • 系统交互流程
  • 关键代码实现
    • 前端加密示例
    • 后端解密示例
    • 本文涉及到的技术组件
  • 常见问题
    • Q1:如何防止重放攻击?
    • Q2:为什么选择过滤器解密?
    • Q2:iOS/Android如何兼容?
    • Q4: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. 服务端对应接口拿到解密后的数据,做后续的业务逻辑处理

系统交互流程

#mermaid-svg-D8JoamNR9QqFJ5cS {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS .error-icon{fill:#552222;}#mermaid-svg-D8JoamNR9QqFJ5cS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D8JoamNR9QqFJ5cS .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-D8JoamNR9QqFJ5cS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D8JoamNR9QqFJ5cS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D8JoamNR9QqFJ5cS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D8JoamNR9QqFJ5cS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D8JoamNR9QqFJ5cS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D8JoamNR9QqFJ5cS .marker.cross{stroke:#333333;}#mermaid-svg-D8JoamNR9QqFJ5cS svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D8JoamNR9QqFJ5cS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D8JoamNR9QqFJ5cS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-D8JoamNR9QqFJ5cS .actor-line{stroke:grey;}#mermaid-svg-D8JoamNR9QqFJ5cS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS .sequenceNumber{fill:white;}#mermaid-svg-D8JoamNR9QqFJ5cS #sequencenumber{fill:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS .messageText{fill:#333;stroke:#333;}#mermaid-svg-D8JoamNR9QqFJ5cS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D8JoamNR9QqFJ5cS .labelText,#mermaid-svg-D8JoamNR9QqFJ5cS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-D8JoamNR9QqFJ5cS .loopText,#mermaid-svg-D8JoamNR9QqFJ5cS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-D8JoamNR9QqFJ5cS .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-D8JoamNR9QqFJ5cS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-D8JoamNR9QqFJ5cS .noteText,#mermaid-svg-D8JoamNR9QqFJ5cS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-D8JoamNR9QqFJ5cS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D8JoamNR9QqFJ5cS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D8JoamNR9QqFJ5cS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-D8JoamNR9QqFJ5cS .actorPopupMenu{position:absolute;}#mermaid-svg-D8JoamNR9QqFJ5cS .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-D8JoamNR9QqFJ5cS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-D8JoamNR9QqFJ5cS .actor-man circle,#mermaid-svg-D8JoamNR9QqFJ5cS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-D8JoamNR9QqFJ5cS :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 前端 过滤器 后端服务 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. 过滤器解密
@Componentpublic 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:如何防止重放攻击?

// 服务端校验noncepublic 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密钥对如何生成

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

扩展阅读

  • RSA算法介绍
  • AES算法介绍

结语

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

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

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