资讯动态

Spring Boot 实战:RSA+AES 接口全链路加解密(防篡改 / 防重放)

发布时间:2026/8/15 12:52:25 来源:尧图企业网站定制
前言本项目演示了如何在 Spring Boot 应用中实现安全的 API 接口加密传输机制采用 RSA AES-GCM 混合加密方案确保敏感数据在传输过程中的机密性和完整性。基于 Spring Boot 的 API 接口加密传输解决方案实现前后端数据的端到端加密通信。1 方案背景为什么HTTPS之外还要做接口加密很多开发者会有疑问我已经用了HTTPS为什么还要做接口级加解密核心答案是HTTPS只解决了传输链路的安全无法覆盖「终端被Hook、App被逆向、API网关日志泄露、内网流量被侧录」等场景的风险。HTTPS是“防路上被偷看”而接口级加密是“防终点被看穿”两者解决的问题完全不同具体对比如下风险场景HTTPS接口级加密传输链路被监听✅ 防护✅ 防护终端被Hook/抓包工具解密❌ 无法防护✅ 防护App被逆向、API逻辑被破解❌ 无法防护✅ 防护网关/代理日志明文泄露❌ 无法防护✅ 防护内网不可信链路流量被侧录❌ 无法防护✅ 防护2 整体方案设计核心特性混合加密方案RSA-OAEP 加密 AES 密钥AES-GCM 加密业务数据防重放攻击基于时间戳 Nonce 的双重防护机制数据完整性校验AES-GCM 模式自带认证标签防止数据篡改注解式开发通过 DecryptRequestBody 和 EncryptResponseBody 注解实现透明加解密前后端兼容与 Web Crypto API 完全兼容支持浏览器原生加密2.1 核心设计目标核心目标落地方案密钥传输安全RSA-OAEP 非对称加密兼容WebCrypto规避老旧填充方式的安全风险业务数据加解密AES-256-GCM 对称加密自带完整性校验CPU指令集加速支持AAD防篡改防请求篡改GCM-AAD 附加认证数据timestampnonce参与认证篡改直接解密失败防请求重放timestamp时间窗口校验 nonce唯一值幂等控制Redis实现业务零侵入自定义注解 参数解析器 响应增强Controller无需感知加解密逻辑跨端兼容性前后端算法100%对齐支持Web/小程序/App多端对接生产可运维支持密钥平滑轮换、统一异常处理、全链路日志追踪2.2 全链路时序流程客户端提前请求服务端获取RSA公钥客户端生成随机AES-256密钥一请求一密钥杜绝密钥泄露风险客户端用AES-GCM加密业务请求数据将timestampnonce作为AAD附加认证数据客户端用服务端RSA公钥通过RSA-OAEP加密本次的AES密钥客户端封装加密后的AES密钥、业务数据、timestamp、nonce为统一请求体发送至服务端服务端校验timestamp时间窗口默认5分钟过期直接拒绝校验nonce是否已使用已使用则判定为重放请求服务端用RSA私钥解密拿到本次请求的AES密钥服务端用AES密钥相同AAD解密业务数据反序列化为对应DTO对象业务处理完成后服务端用本次请求的AES密钥加密响应数据返回给客户端客户端用本地保存的AES密钥解密响应数据拿到业务结果2.3 项目工程结构代码见 gitee: Trial5-api-security-demosrc/main/java/com/storm/security/ ├── annotation/ // 自定义注解 │ ├── DecryptRequestBody.java // 请求解密注解 │ └── EncryptResponseBody.java // 响应加密注解 ├── config/ // 配置类 │ ├── EncryptionConfig.java // 密钥、工具类Bean配置 │ └── WebMvcConfig.java // 参数解析器、响应增强注册 ├── constant/ // 常量类 │ └── EncryptionConstant.java // 算法、超时时间等常量 ├── controller/ // 业务接口 │ ├── KeyController.java // 公钥获取接口 │ └── UserController.java // 业务测试接口 ├── dto/ // 数据传输对象 │ ├── EncryptedRequest.java // 统一加密请求体 │ ├── EncryptedResponse.java // 统一加密响应体 │ └── UserDTO.java // 业务DTO示例 ├── encryption/ // 加解密核心工具 │ ├── RsaUtil.java // RSA-OAEP加解密工具 │ ├── AesUtil.java // AES-GCM加解密工具 │ └── KeyPairManager.java // RSA密钥对管理支持轮换 ├── exception/ // 异常处理 │ ├── EncryptionException.java // 加解密自定义异常 │ └── GlobalExceptionHandler.java// 全局异常统一处理 ├── resolver/ // 参数解析器 │ └── DecryptRequestBodyResolver.java // 自动解密参数解析器 ├── advice/ // 响应增强 │ └── EncryptResponseBodyAdvice.java // 自动加密响应增强 └── service/ // 核心调度 └── EncryptionService.java // 加解密核心调度服务3 核心技术选型说明3.1 RSA算法必须统一RSA-OAEP拒绝PKCS1Padding大量教程都会踩的核心坑Java默认的RSA/ECB/PKCS1Padding与前端WebCrypto API默认的RSA-OAEP完全不兼容且PKCS1Padding属于老旧填充方式存在明文攻击风险已被NIST不推荐使用。✅ 生产级正确做法前后端统一使用RSA/ECB/OAEPWithSHA-256AndMGF1Padding完全兼容WebCrypto安全性更高。3.2 AES算法必须选GCM模式拒绝CBC/CTRAES模式是否推荐核心问题AES-CBC❌ 不推荐仅提供加密无完整性校验存在Padding Oracle攻击风险AES-CTR❌ 不推荐流加密模式无完整性校验无法防篡改AES-GCM✅ 生产推荐同时实现「加密完整性认证防篡改」CPU有指令集加速性能优异天然支持AAD附加认证数据GCM的AAD特性是防篡改的核心我们将timestamp和nonce作为AAD数据不参与加密但参与认证一旦这两个字段被篡改解密会直接抛出AEADBadTagException从根本上杜绝请求被篡改的风险。4 完整代码实现采用 SpringBoot3.x Java17 完成4.1 核心依赖pom.xml!-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring Data Redis防重放nonce存储 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- Lombok简化代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Jackson JSON处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency4.2 常量类统一管理魔法值public class EncryptionConstant { // RSA算法配置 public static final String RSA_ALGORITHM RSA/ECB/OAEPWithSHA-256AndMGF1Padding; public static final int RSA_KEY_SIZE 2048; // AES算法配置 public static final String AES_ALGORITHM AES/GCM/NoPadding; public static final int AES_KEY_SIZE 256; public static final int GCM_IV_LENGTH 12; public static final int GCM_TAG_LENGTH 128; // 防重放配置 public static final long REQUEST_EXPIRE_TIME 5 * 60 * 1000L; public static final long NONCE_EXPIRE_TIME 5; public static final TimeUnit NONCE_EXPIRE_UNIT TimeUnit.MINUTES; public static final String NONCE_REDIS_KEY_PREFIX security:nonce:; // 字符集 public static final Charset CHARSET StandardCharsets.UTF_8; }4.3 自定义异常类public class EncryptionException extends RuntimeException { public EncryptionException(String message) { super(message); } public EncryptionException(String message, Throwable cause) { super(message, cause); } }4.4 RSA-OAEP加解密工具类Slf4j public class RsaUtil { public String encrypt(String data, Key publicKey) { try { Cipher cipher Cipher.getInstance(EncryptionConstant.RSA_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedData cipher.doFinal(data.getBytes(EncryptionConstant.CHARSET)); return Base64.getEncoder().encodeToString(encryptedData); } catch (Exception e) { log.error(RSA加密失败, e); throw new EncryptionException(RSA加密失败, e); } } public String decrypt(String encryptedData, Key privateKey) { try { Cipher cipher Cipher.getInstance(EncryptionConstant.RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decodedData Base64.getDecoder().decode(encryptedData); byte[] decryptedData cipher.doFinal(decodedData); return new String(decryptedData, EncryptionConstant.CHARSET); } catch (Exception e) { log.error(RSA解密失败, e); throw new EncryptionException(RSA解密失败密钥不匹配或数据被篡改, e); } } }4.5 AES-GCM加解密工具类Slf4j public class AesUtil { public String generateAesKey() { byte[] key new byte[EncryptionConstant.AES_KEY_SIZE / 8]; new SecureRandom().nextBytes(key); return Base64.getEncoder().encodeToString(key); } public String encrypt(String data, String base64Key, String aad) { try { byte[] keyBytes Base64.getDecoder().decode(base64Key); if (keyBytes.length ! EncryptionConstant.AES_KEY_SIZE / 8) { throw new EncryptionException(AES密钥长度必须为256位); } SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); byte[] iv new byte[EncryptionConstant.GCM_IV_LENGTH]; new SecureRandom().nextBytes(iv); Cipher cipher Cipher.getInstance(EncryptionConstant.AES_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(EncryptionConstant.GCM_TAG_LENGTH, iv)); cipher.updateAAD(aad.getBytes(EncryptionConstant.CHARSET)); byte[] encryptedData cipher.doFinal(data.getBytes(EncryptionConstant.CHARSET)); byte[] result new byte[iv.length encryptedData.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(encryptedData, 0, result, iv.length, encryptedData.length); return Base64.getEncoder().encodeToString(result); } catch (EncryptionException e) { throw e; } catch (Exception e) { log.error(AES加密失败, e); throw new EncryptionException(AES加密失败, e); } } public String decrypt(String encryptedData, String base64Key, String aad) { try { byte[] keyBytes Base64.getDecoder().decode(base64Key); if (keyBytes.length ! EncryptionConstant.AES_KEY_SIZE / 8) { throw new EncryptionException(AES密钥长度必须为256位); } SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); byte[] decodedData Base64.getDecoder().decode(encryptedData); byte[] iv new byte[EncryptionConstant.GCM_IV_LENGTH]; byte[] cipherBody new byte[decodedData.length - EncryptionConstant.GCM_IV_LENGTH]; System.arraycopy(decodedData, 0, iv, 0, iv.length); System.arraycopy(decodedData, iv.length, cipherBody, 0, cipherBody.length); Cipher cipher Cipher.getInstance(EncryptionConstant.AES_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(EncryptionConstant.GCM_TAG_LENGTH, iv)); cipher.updateAAD(aad.getBytes(EncryptionConstant.CHARSET)); byte[] decryptedData cipher.doFinal(cipherBody); return new String(decryptedData, EncryptionConstant.CHARSET); } catch (EncryptionException e) { throw e; } catch (Exception e) { log.error(AES解密失败, e); throw new EncryptionException(AES解密失败密钥不匹配、数据被篡改或AAD不一致, e); } } }4.6 RSA密钥对管理支持平滑轮换Getter public class KeyPairManager { private final RSAPublicKey publicKey; private final RSAPrivateKey privateKey; private final String publicKeyBase64; // 生产环境建议从配置中心/KMS加载支持热更新与平滑轮换 public KeyPairManager() throws Exception { KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(RSA); keyPairGenerator.initialize(EncryptionConstant.RSA_KEY_SIZE); KeyPair keyPair keyPairGenerator.generateKeyPair(); this.publicKey (RSAPublicKey) keyPair.getPublic(); this.privateKey (RSAPrivateKey) keyPair.getPrivate(); this.publicKeyBase64 Base64.getEncoder().encodeToString(publicKey.getEncoded()); } }4.7 配置类Configuration public class EncryptionConfig { Bean public KeyPairManager keyPairManager() throws Exception { return new KeyPairManager(); } Bean public RsaUtil rsaUtil() { return new RsaUtil(); } Bean public AesUtil aesUtil() { return new AesUtil(); } }4.8 统一请求与响应DTO// 加密请求体 Data Builder NoArgsConstructor AllArgsConstructor public class EncryptedRequest { private String encryptedKey; private String encryptedData; private Long timestamp; private String nonce; } // 加密响应体 package com.example.security.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; Data Builder NoArgsConstructor AllArgsConstructor public class EncryptedResponse { private String encryptedData; private int code; private String msg; } // 业务DTO示例 package com.example.security.dto; import lombok.Data; Data public class UserDTO { private String username; private String password; private String phone; private String email; }4.9 自定义注解// 请求解密注解 Target(ElementType.PARAMETER) Retention(RetentionPolicy.RUNTIME) public interface DecryptRequestBody { } // 响应加密注解 Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface EncryptResponseBody { }4.10 核心加解密调度服务Slf4j Service RequiredArgsConstructor public class EncryptionService { private final KeyPairManager keyPairManager; private final RsaUtil rsaUtil; private final AesUtil aesUtil; private final ObjectMapper objectMapper; private final StringRedisTemplate redisTemplate; private static final ThreadLocalString AES_KEY_HOLDER new ThreadLocal(); private static final ThreadLocalString AAD_HOLDER new ThreadLocal(); // 请求解密 public T T decrypt(EncryptedRequest request, ClassT targetClass) { try { // 时间窗口校验 long currentTime System.currentTimeMillis(); if (Math.abs(currentTime - request.getTimestamp()) EncryptionConstant.REQUEST_EXPIRE_TIME) { throw new EncryptionException(请求已过期请重新发起); } // nonce幂等校验 String nonceKey EncryptionConstant.NONCE_REDIS_KEY_PREFIX request.getNonce(); Boolean isFirstRequest redisTemplate.opsForValue() .setIfAbsent(nonceKey, 1, EncryptionConstant.NONCE_EXPIRE_TIME, EncryptionConstant.NONCE_EXPIRE_UNIT); if (Boolean.FALSE.equals(isFirstRequest)) { throw new EncryptionException(重复请求禁止访问); } // RSA解密获取AES密钥 String aesKey rsaUtil.decrypt(request.getEncryptedKey(), keyPairManager.getPrivateKey()); // 构建AAD与前端完全一致 String aad request.getTimestamp() : request.getNonce(); // AES解密业务数据 String jsonData aesUtil.decrypt(request.getEncryptedData(), aesKey, aad); // 存入ThreadLocal用于响应加密 AES_KEY_HOLDER.set(aesKey); AAD_HOLDER.set(aad); // 反序列化为目标对象 return objectMapper.readValue(jsonData, targetClass); } catch (EncryptionException e) { throw e; } catch (Exception e) { log.error(请求解密失败, e); throw new EncryptionException(请求解密失败, e); } } // 响应加密 public String encrypt(Object data) { try { String aesKey AES_KEY_HOLDER.get(); String aad AAD_HOLDER.get(); if (aesKey null || aad null) { throw new EncryptionException(响应加密失败未获取到请求密钥信息); } String jsonData objectMapper.writeValueAsString(data); return aesUtil.encrypt(jsonData, aesKey, aad); } catch (EncryptionException e) { throw e; } catch (Exception e) { log.error(响应加密失败, e); throw new EncryptionException(响应加密失败, e); } finally { // 清除ThreadLocal避免内存泄漏 AES_KEY_HOLDER.remove(); AAD_HOLDER.remove(); } } }4.11 自动解密参数解析器RequiredArgsConstructor public class DecryptRequestBodyResolver implements HandlerMethodArgumentResolver { private final EncryptionService encryptionService; private final ObjectMapper objectMapper; Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(DecryptRequestBody.class); } Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request webRequest.getNativeRequest(HttpServletRequest.class); EncryptedRequest encryptedRequest objectMapper.readValue(request.getInputStream(), EncryptedRequest.class); return encryptionService.decrypt(encryptedRequest, parameter.getParameterType()); } }4.12 自动加密响应增强RestControllerAdvice RequiredArgsConstructor public class EncryptResponseBodyAdvice implements ResponseBodyAdviceObject { private final EncryptionService encryptionService; Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { return returnType.hasMethodAnnotation(EncryptResponseBody.class) || returnType.getContainingClass().isAnnotationPresent(EncryptResponseBody.class); } Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class? extends HttpMessageConverter? selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { String encryptedData encryptionService.encrypt(body); return EncryptedResponse.builder() .encryptedData(encryptedData) .code(200) .msg(success) .build(); } }4.13 WebMvc配置与全局异常处理// WebMvc配置 Configuration RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { private final EncryptionService encryptionService; private final ObjectMapper objectMapper; Override public void addArgumentResolvers(ListHandlerMethodArgumentResolver resolvers) { resolvers.add(new DecryptRequestBodyResolver(encryptionService, objectMapper)); } } // 全局异常处理 Slf4j RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(EncryptionException.class) public ResponseEntityMapString, Object handleEncryptionException(EncryptionException e) { log.error(加解密异常{}, e.getMessage(), e); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(Map.of(code, 400, msg, e.getMessage())); } ExceptionHandler(Exception.class) public ResponseEntityMapString, Object handleException(Exception e) { log.error(系统异常{}, e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of(code, 500, msg, 系统内部错误)); } }4.14 业务Controller示例// 公钥获取接口 RestController RequestMapping(/api/key) RequiredArgsConstructor public class KeyController { private final KeyPairManager keyPairManager; GetMapping(/public) public ResponseEntityMapString, String getPublicKey() { return ResponseEntity.ok(Map.of(publicKey, keyPairManager.getPublicKeyBase64())); } } // 业务接口 RestController RequestMapping(/api/user) public class UserController { PostMapping(/register) EncryptResponseBody public ResponseEntityMapString, Object register(DecryptRequestBody UserDTO user) { // 业务逻辑处理 System.out.println(收到用户注册请求 user); return ResponseEntity.ok(Map.of( msg, 注册成功, username, user.getUsername(), userId, System.currentTimeMillis() )); } }5 前端完整对接代码1、获取公钥const response await fetch(/api/key/public); const { publicKey } await response.json();2、加密请求// 生成 AES 密钥 const aesKey window.crypto.getRandomValues(new Uint8Array(32)); // 导入 RSA 公钥 const publicKeyObj await window.crypto.subtle.importKey( spki, base64ToArrayBuffer(publicKey), { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); // RSA 加密 AES 密钥 const encryptedKey await window.crypto.subtle.encrypt( { name: RSA-OAEP }, publicKeyObj, aesKey ); // 生成时间戳和 Nonce const timestamp Date.now(); const nonce crypto.randomUUID(); const aad ${timestamp}:${nonce}; // AES-GCM 加密业务数据 const iv window.crypto.getRandomValues(new Uint8Array(12)); const aesKeyObj await window.crypto.subtle.importKey( raw, aesKey, { name: AES-GCM }, false, [encrypt] ); const encryptedData await window.crypto.subtle.encrypt( { name: AES-GCM, iv: iv, additionalData: new TextEncoder().encode(aad), tagLength: 128 }, aesKeyObj, new TextEncoder().encode(JSON.stringify(userData)) ); // 发送请求 const response await fetch(/api/user/register, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ encryptedKey: arrayBufferToBase64(encryptedKey), encryptedData: arrayBufferToBase64( concatenate(iv, new Uint8Array(encryptedData)) ), timestamp: timestamp, nonce: nonce }) });6 生产级注意事项与踩坑指南6.1 核心安全边界本方案仅解决数据传输的加密、防篡改、防重放不能替代身份认证体系需与JWT、OAuth2等身份认证方案结合使用。HTTPS是基础本方案是HTTPS之上的加固绝对不能因为用了接口加密就关闭HTTPS。不适合大文件传输、日志采集等高并发大流量场景加解密会有性能开销。6.2 密钥管理优化生产环境禁止启动时生成密钥对需将密钥对配置在配置中心Nacos/Apollo支持热更新与平滑轮换建议3个月更换一次密钥。高安全场景建议使用KMS密钥管理服务不要将密钥硬编码在代码或配置文件中。6.3 常见问题排查AES解密失败AEADBadTagException最常见原因前后端AAD不一致检查timestampnonce的拼接顺序、分隔符是否完全一致 其他原因IV长度不是12字节、密钥不匹配、数据被篡改RSA解密失败Data must not be longer than 245 bytes原因RSA仅用于加密AES密钥禁止用RSA加密业务数据2048位RSA最多加密245字节数据 其他原因前后端填充方式不一致必须统一RSA-OAEP防重放误判原因客户端重试未更换nonce、前后端时间差超过5分钟、Redis配置错误导致nonce未正确写入6.4 性能评估操作平均耗时RSA-OAEP 2048位解密~0.8msAES-256-GCM加解密1KB以内数据0.1ms全链路加解密含Redis校验~1-2ms4核8G服务器单接口QPS3000常规业务系统QPS 1000-5000完全可以无压力使用性能开销完全可控。7 总结如果你的系统只是普通的Web管理系统HTTPS已经足够满足安全需求但如果你面对的是高敏感数据传输、App/小程序开发、需要防逆向、防Hook、防网关日志泄露的场景接口级加解密不是过度设计而是必要的安全防线。本文提供的方案从算法选型、工程化设计、前后端兼容、生产踩坑指南等多个维度实现了一套可直接落地生产的全链路加解密方案无需修改业务代码只需添加注解即可实现接口的自动加解密。更多容易可以看仓库文档快速开始我的仓库地址gitee.com/liyongde/ja…转发请携带作者信息怒放吧德德 一个有梦有戏的人持续创作很不容易作者将以尽可能的详细把所学知识分享各位开发者一起进步一起学习。转载请携带链接转载到微信公众号请勿选择原创谢谢创作不易如有错误请指正感谢观看记得点赞哦谢谢支持原文链接https://juejin.cn/post/7610966377898311720

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价