Spring Boot API安全进阶:RSA+AES混合加密方案设计与实现
发布时间:2026/6/30 18:59:53
分类:文化教育
浏览:1234

1. 项目概述与核心痛点最近在review团队里几个Spring Boot项目的API安全方案发现一个挺普遍的现象超过八成的接口所谓的“安全”就是简单挂个JWTJSON Web Token。开发同学觉得有了Token校验用户身份再配合HTTPS好像就万事大吉了。但几次内部渗透测试的结果给我们敲了警钟——传输过程中的数据明文暴露、Token被截获后的重放攻击、甚至因密钥管理不当导致的Token伪造这些风险在仅依赖JWT的方案里几乎都是敞开的门。JWT的本质是签名的JSON它解决了无状态认证和授权信息传递的问题这没错。但它通常不负责加密。一个典型的JWT由Header、Payload、Signature三部分组成前两部分仅仅是Base64编码任何人都可以解码看到里面的信息。虽然Signature能防止数据被篡改但防止不了被窥探。如果你的API传输的是用户手机号、身份证号、余额等敏感信息仅用JWT就等于把这些数据“写”在了明信片上邮寄。所以这个项目的目标很明确在Spring Boot架构下设计并实现一套超越单纯JWT的、对传输内容进行强加密的API安全方案。核心思路是引入非对称加密RSA与对称加密AES的混合加密模式确保数据从离开客户端到被服务端解析全程处于密文状态。同时我们还要妥善解决密钥管理、防重放、数据完整性校验等一系列衍生问题。下面我就把我们在生产环境中打磨了两年多的这套方案从设计思路到代码落地完整地拆解一遍。2. 混合加密方案的整体设计思路为什么是RSA AES的混合模式而不是直接用RSA或直接用AES这得从两种加密算法的特性说起。RSA属于非对称加密有一对密钥公钥和私钥。公钥可以公开用来加密数据私钥必须严格保密用来解密。它的优点是密钥分发安全但致命缺点是加解密速度慢尤其不适合加密大数据量。AES则是对称加密加密和解密使用同一把密钥速度极快适合处理大量数据但密钥如何在通信双方安全地共享是个难题。混合加密正是取两者之长。其核心流程可以概括为“一次非对称握手后续对称通话”密钥协商阶段非对称客户端持有服务端发布的RSA公钥。当需要发起请求时客户端随机生成一个高质量的AES密钥我们称之为sessionKey。然后用RSA公钥加密这个sessionKey得到密文encryptedSessionKey。数据加密阶段对称客户端使用刚刚生成的sessionKey采用AES算法对实际的业务请求体JSON数据进行加密得到密文encryptedData。请求发送客户端将encryptedSessionKey和encryptedData一同发送给服务端。注意整个过程中原始的sessionKey和业务数据在传输链路上从未以明文形式出现。服务端解密服务端用自己保管的RSA私钥解密encryptedSessionKey还原出sessionKey。再用这个sessionKey解密encryptedData得到原始的明文业务数据。这个设计巧妙地将RSA的安全密钥交换能力与AES的高效数据加密能力结合了起来。每次会话甚至每次请求都可以使用不同的AES密钥实现了前向安全。即便某一次的AES密钥被破解也不会影响其他会话的安全。注意这里有一个关键决策点——AES密钥的生命周期。可以是“一次请求一密钥”最安全但性能开销稍大也可以是“一个会话周期一密钥”如用户登录后到登出前。我们目前生产环境采用的是“一次请求一密钥”因为对于现代服务器和客户端设备生成一个AES密钥并做一次RSA加密的解密开销在API交互的尺度上是可以接受的它最大限度地减少了密钥被重复使用的风险。3. 核心组件与依赖准备在动手写代码之前我们需要把核心的依赖和基础组件准备好。我们的技术栈是Spring Boot 3.xJDK 17。首先在pom.xml中引入必要的依赖。除了Spring Boot的基础starter我们主要需要两个库一个用于处理RSA/AES加解密另一个用于处理JSON。dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 加解密工具库这里选用功能全面且稳定的Hutool -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- 配置处理 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency /dependencies选择Hutool是因为它封装了JDK原生的加解密API提供了更友好、更不易出错的接口并且支持PKCS#1、PKCS#8等多种密钥格式避免了直接使用KeyPairGenerator和Cipher时容易遇到的陷阱。接下来我们要生成RSA密钥对。密钥对的生成应该在服务端部署时完成并且私钥必须被妥善保管如放入配置中心、KMS或受保护的服务器文件中绝不能提交到代码仓库。这里我们用一段简单的Java代码来生成实际生产中可能使用openssl命令。import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.RSA; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; public class KeyGenerator { public static void main(String[] args) { // 生成RSA密钥对密钥长度建议2048位及以上 RSA rsa new RSA(2048); // 获取公钥和私钥的Base64编码字符串 String publicKeyBase64 rsa.getPublicKeyBase64(); String privateKeyBase64 rsa.getPrivateKeyBase64(); System.out.println(公钥给客户端:); System.out.println(publicKeyBase64); System.out.println(\n私钥服务端保管绝不可泄露:); System.out.println(privateKeyBase64); } }运行这段代码你会得到一对Base64编码的字符串。公钥可以安全地提供给客户端例如通过一个特定的API接口/api/public-key暴露私钥则需要配置到服务端应用的环境变量或配置文件中。4. 服务端核心实现详解服务端的实现是整个方案的大脑主要负责提供公钥、解密请求、处理业务、加密响应。我们会通过几个核心组件来实现。4.1 配置与密钥管理首先我们通过ConfigurationProperties来管理加解密相关的配置这样可以通过application.yml灵活调整。import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Data Component ConfigurationProperties(prefix api.encrypt) public class ApiEncryptProperties { /** * RSA私钥Base64格式 */ private String privateKey; /** * AES加密模式默认CBC */ private String aesMode CBC; /** * AES填充方式默认PKCS5Padding */ private String aesPadding PKCS5Padding; /** * 是否开启请求响应加解密默认开启 */ private boolean enabled true; }对应的application.yml配置api: encrypt: private-key: 你的RSA私钥Base64字符串 enabled: true密钥的加载和管理我们放在一个Bean中应用启动时即初始化RSA和AES工具实例。import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.asymmetric.RSA; import cn.hutool.crypto.symmetric.AES; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; Slf4j Configuration public class CryptoConfig { Autowired private ApiEncryptProperties properties; private RSA rsa; /** * 初始化RSA实例仅持有私钥用于解密客户端传来的AES密钥 */ PostConstruct public void initRsa() { if (!properties.isEnabled()) { log.warn(API加解密功能未启用); return; } if (StrUtil.isBlank(properties.getPrivateKey())) { throw new IllegalArgumentException(API加密私钥未配置); } try { // 使用私钥字符串初始化RSA对象此对象只能用于解密和签名 this.rsa new RSA(properties.getPrivateKey(), null); log.info(RSA解密器初始化成功); } catch (Exception e) { log.error(RSA解密器初始化失败, e); throw new RuntimeException(加密配置初始化失败, e); } } Bean public RSA rsaDecryptor() { return this.rsa; } /** * 动态创建AES解密器 * param sessionKey 客户端传来的、经RSA解密后的AES密钥字节数组 * return AES解密器实例 */ public AES createAesDecryptor(byte[] sessionKey) { // 根据配置构建AES对象这里以CBC/PKCS5Padding为例 // 注意AES CBC模式需要IV初始化向量通常由客户端随机生成并随加密数据一起传来 // 为了简化示例这里假设使用ECB模式不推荐或由客户端固定IV。实际生产环境必须使用CBC/CTR等模式并安全传递IV。 // 更安全的做法是客户端将IV和加密数据一起用RSA公钥加密后传来。 // 本例为演示核心流程暂不处理IV实际应用请务必补充。 Mode mode Mode.valueOf(properties.getAesMode()); Padding padding Padding.valueOf(properties.getAesPadding()); // 这里需要根据sessionKey和可能的IV构建AES对象具体构造方法需参考Hutool文档 // 示例return new AES(mode, padding, sessionKey); // 由于IV处理是另一个关键点此处省略具体构造下文在解密流程中会详细说明。 return null; // 占位 } }实操心得密钥存储安全私钥的存储是生命线。绝对不要硬编码在代码里或提交到Git。我们线上使用的是结合配置中心如Nacos和KMS密钥管理服务的方案。应用启动时从配置中心获取一个加密的私钥密文再用KMS的解密权限获取真正的私钥明文。这样即使配置中心被攻破攻击者拿到的也是无法直接使用的密文。4.2 统一请求/响应体设计为了标准化通信协议我们需要定义客户端和服务端交互的数据结构。加密请求体import lombok.Data; import javax.validation.constraints.NotBlank; Data public class EncryptedRequest { /** * 经过RSA公钥加密后的AES密钥Base64编码 */ NotBlank(message “加密密钥不能为空”) private String encryptedKey; /** * 使用上述AES密钥加密后的业务数据Base64编码 */ NotBlank(message “加密数据不能为空”) private String encryptedData; /** * 时间戳用于防重放 */ private Long timestamp; /** * 随机数用于防重放 */ private String nonce; /** * 签名用于验证数据完整性可选但推荐 */ private String sign; }加密响应体import lombok.Data; Data public class EncryptedResponse { /** * 响应状态码 */ private int code; /** * 响应消息 */ private String msg; /** * 加密后的业务数据Base64编码。如果响应无数据体此字段可为空。 */ private String encryptedData; /** * 本次响应使用的AES密钥用客户端公钥加密(通常不需要沿用请求的sessionKey或新生成) * 更常见的做法是响应数据使用请求中的sessionKey加密或服务端生成新的临时AES密钥并用客户端公钥加密后返回。 * 我们采用简单策略响应使用请求中的sessionKey加密。 */ // private String encryptedKey; }4.3 核心解密/加密过滤器Interceptor我们将加解密逻辑放在Spring的拦截器Interceptor中这样可以非侵入式地处理所有经过Controller的请求和响应。这里选择拦截器而非过滤器Filter是因为拦截器能更自然地融入Spring MVC的生命周期更容易获取到HandlerMethod等上下文信息。import cn.hutool.core.codec.Base64; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.symmetric.AES; import cn.hutool.json.JSONUtil; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.lang.reflect.Type; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; Slf4j Component public class ApiEncryptInterceptor implements HandlerInterceptor { Autowired private RSA rsaDecryptor; Autowired private ApiEncryptProperties properties; Autowired private ObjectMapper objectMapper; // 简单的防重放缓存生产环境建议用Redis并设置过期时间 private MapString, Long nonceCache new ConcurrentHashMap(); private static final long TIMESTAMP_TOLERANCE 5 * 60 * 1000; // 5分钟容忍度 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!properties.isEnabled() || !isEncryptRequired(handler)) { return true; // 未启用加密或该接口不需要加密直接放行 } // 1. 读取并解析加密请求体 String body getRequestBody(request); EncryptedRequest encryptedRequest; try { encryptedRequest objectMapper.readValue(body, EncryptedRequest.class); } catch (Exception e) { log.warn(“加密请求体解析失败”, e); returnErrorResponse(response, “非法请求格式”); return false; } // 2. 防重放校验 if (!checkReplayAttack(encryptedRequest)) { returnErrorResponse(response, “请求已过期或重复”); return false; } // 3. RSA解密获取AES sessionKey byte[] sessionKeyBytes; try { byte[] encryptedKeyBytes Base64.decode(encryptedRequest.getEncryptedKey()); sessionKeyBytes rsaDecryptor.decrypt(encryptedKeyBytes, KeyType.PrivateKey); // 建议校验解密出的sessionKey长度如16, 24, 32字节对应AES-128,192,256 if (sessionKeyBytes.length ! 16 sessionKeyBytes.length ! 24 sessionKeyBytes.length ! 32) { throw new RuntimeException(“解密得到的会话密钥长度非法”); } } catch (Exception e) { log.error(“RSA解密会话密钥失败”, e); returnErrorResponse(response, “解密失败”); return false; } // 4. 使用sessionKey解密业务数据 String decryptedDataStr; try { // 这里是关键需要根据AES模式和可能的IV来构造解密器。 // 假设客户端使用AES/CBC/PKCS5Padding且IV拼接在encryptedData的前16个字节。 byte[] encryptedDataBytes Base64.decode(encryptedRequest.getEncryptedData()); // 分离IV和实际密文约定前16字节为IV byte[] iv new byte[16]; byte[] actualCipherText new byte[encryptedDataBytes.length - 16]; System.arraycopy(encryptedDataBytes, 0, iv, 0, 16); System.arraycopy(encryptedDataBytes, 16, actualCipherText, 0, actualCipherText.length); // 使用sessionKey和IV创建AES解密器 AES aes new AES(Mode.CBC, Padding.PKCS5Padding, sessionKeyBytes, iv); byte[] decryptedBytes aes.decrypt(actualCipherText); decryptedDataStr new String(decryptedBytes, StandardCharsets.UTF_8); } catch (Exception e) { log.error(“AES解密业务数据失败”, e); returnErrorResponse(response, “业务数据解密失败”); return false; } // 5. 将解密后的JSON字符串设置回请求属性供后续的RequestBody反序列化使用 // 这里需要“偷梁换柱”替换掉HttpServletRequest中的输入流。 // 我们可以使用HttpServletRequest的装饰器模式或者更简单地将解密后的字符串存入request attribute。 request.setAttribute(“DECRYPTED_REQUEST_BODY”, decryptedDataStr); // 同时将本次会话的sessionKey也存起来用于后续加密响应 request.setAttribute(“SESSION_KEY_BYTES”, sessionKeyBytes); // 将nonce加入缓存防止本次请求被重放 nonceCache.put(encryptedRequest.getNonce(), System.currentTimeMillis()); return true; } private boolean checkReplayAttack(EncryptedRequest request) { // 1. 检查时间戳是否在可接受范围内 long currentTime System.currentTimeMillis(); if (Math.abs(currentTime - request.getTimestamp()) TIMESTAMP_TOLERANCE) { log.warn(“请求时间戳超出容忍范围clientTs:{}, serverTs:{}”, request.getTimestamp(), currentTime); return false; } // 2. 检查nonce是否已使用过 if (nonceCache.containsKey(request.getNonce())) { log.warn(“检测到重复的随机数: {}”, request.getNonce()); return false; } // 3. (可选)校验签名确保encryptedKey和encryptedData未被篡改 // if (!verifySign(request)) { return false; } return true; } private void returnErrorResponse(HttpServletResponse response, String msg) throws IOException { response.setContentType(“application/json;charsetUTF-8”); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); EncryptedResponse errorResp new EncryptedResponse(); errorResp.setCode(400); errorResp.setMsg(msg); objectMapper.writeValue(response.getWriter(), errorResp); } // 其他辅助方法getRequestBody, isEncryptRequired, verifySign 等省略... }拦截器写好之后需要在Web配置中注册它并指定其拦截的路径。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Autowired private ApiEncryptInterceptor apiEncryptInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { // 拦截所有/api/开头的请求排除获取公钥等无需加密的接口 registry.addInterceptor(apiEncryptInterceptor) .addPathPatterns(“/api/**”) .excludePathPatterns(“/api/public-key”, “/api/health”); } }4.4 响应加密与Controller示例请求解密后业务Controller可以像平常一样工作接收明文对象。难点在于如何将Controller返回的对象自动加密。我们可以在拦截器的postHandle或使用ResponseBodyAdvice来实现。这里使用ResponseBodyAdvice更优雅它可以对ResponseBody注解的方法返回值进行统一处理。import cn.hutool.core.codec.Base64; import cn.hutool.crypto.symmetric.AES; import cn.hutool.json.JSONUtil; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; ControllerAdvice(basePackages “com.yourpackage.api”) public class EncryptResponseBodyAdvice implements ResponseBodyAdviceObject { Autowired private ObjectMapper objectMapper; Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { // 只处理标注了EncryptResponse注解的方法或者所有api接口 // 这里我们选择处理所有返回值不为空的接口 return true; } Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class? extends HttpMessageConverter? selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 从当前请求线程中获取之前解密时存储的sessionKey // 注意这里需要将ServerHttpRequest转换为Servlet请求以获取attribute略复杂。 // 更简单的做法是在拦截器中将sessionKey存入ThreadLocal。 // 我们采用ThreadLocal方案。 byte[] sessionKeyBytes SessionKeyHolder.get(); if (sessionKeyBytes null || body instanceof EncryptedResponse) { // 如果没有sessionKey如非加密接口或者已经是EncryptedResponse如错误响应直接返回 return body; } try { // 1. 将业务对象序列化为JSON字符串 String dataJson objectMapper.writeValueAsString(body); // 2. 使用sessionKey加密数据 // 注意这里也需要生成一个随机的IV并拼接到密文前。 AES aes SessionKeyHolder.getAesInstance(); // 假设ThreadLocal也存储了AES实例 byte[] iv aes.getIV(); // 获取加密时使用的IV byte[] encryptedBytes aes.encrypt(dataJson.getBytes(StandardCharsets.UTF_8)); // 合并IV和密文 byte[] finalCipherText new byte[iv.length encryptedBytes.length]; System.arraycopy(iv, 0, finalCipherText, 0, iv.length); System.arraycopy(encryptedBytes, 0, finalCipherText, iv.length, encryptedBytes.length); String encryptedDataBase64 Base64.encode(finalCipherText); // 3. 构造加密响应体 EncryptedResponse encryptedResponse new EncryptedResponse(); encryptedResponse.setCode(200); encryptedResponse.setMsg(“success”); encryptedResponse.setEncryptedData(encryptedDataBase64); return encryptedResponse; } catch (Exception e) { log.error(“响应数据加密失败”, e); // 加密失败返回错误信息不加密 return new EncryptedResponse(500, “服务器加密错误”, null); } finally { // 清除ThreadLocal防止内存泄漏 SessionKeyHolder.clear(); } } }最后一个普通的Controller看起来是这样的import org.springframework.web.bind.annotation.*; RestController RequestMapping(“/api/user”) public class UserController { PostMapping(“/info”) public UserInfo getUserInfo(RequestBody UserQuery query) { // 这里的query对象已经是拦截器解密后的明文对象了 // 业务逻辑处理... UserInfo info userService.getInfo(query.getUserId()); return info; // 返回的对象会被ResponseBodyAdvice自动加密 } GetMapping(“/public-key”) public String getPublicKey() { // 返回RSA公钥供客户端加密使用 return “你的RSA公钥Base64字符串”; } }5. 客户端实现要点与示例服务端准备好了客户端如Android、iOS、Web前端也需要配套实现。核心流程是获取公钥 - 生成AES密钥 - 加密数据 - 组装请求。这里以Java例如用于测试或后端调用客户端为例import cn.hutool.core.codec.Base64; import cn.hutool.core.util.RandomUtil; import cn.hutool.crypto.asymmetric.RSA; import cn.hutool.crypto.symmetric.AES; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; public class ApiEncryptClient { private String serverPublicKeyBase64; private String baseUrl; public ApiEncryptClient(String baseUrl, String serverPublicKeyBase64) { this.baseUrl baseUrl; this.serverPublicKeyBase64 serverPublicKeyBase64; } public String postEncryptedData(String apiPath, Object requestBody) throws Exception { // 1. 准备RSA加密器使用服务端公钥 RSA rsa new RSA(null, serverPublicKeyBase64); // 2. 随机生成AES密钥 (例如 AES-256需要32字节) byte[] sessionKey RandomUtil.randomBytes(32); // 3. 用RSA公钥加密AES密钥 byte[] encryptedSessionKey rsa.encrypt(sessionKey, KeyType.PublicKey); String encryptedKeyBase64 Base64.encode(encryptedSessionKey); // 4. 使用AES密钥加密业务数据 // 4.1 生成随机IV16字节 for AES CBC byte[] iv RandomUtil.randomBytes(16); AES aes new AES(Mode.CBC, Padding.PKCS5Padding, sessionKey, iv); String requestBodyJson “将requestBody对象转为JSON字符串”; byte[] encryptedData aes.encrypt(requestBodyJson.getBytes(StandardCharsets.UTF_8)); // 4.2 将IV拼接到密文前 byte[] finalCipherData new byte[iv.length encryptedData.length]; System.arraycopy(iv, 0, finalCipherData, 0, iv.length); System.arraycopy(encryptedData, 0, finalCipherData, iv.length, encryptedData.length); String encryptedDataBase64 Base64.encode(finalCipherData); // 5. 组装加密请求体 MapString, Object encryptedRequest new HashMap(); encryptedRequest.put(“encryptedKey”, encryptedKeyBase64); encryptedRequest.put(“encryptedData”, encryptedDataBase64); encryptedRequest.put(“timestamp”, System.currentTimeMillis()); encryptedRequest.put(“nonce”, RandomUtil.randomString(16)); // 6. 发送HTTP请求 (使用OkHttp, HttpClient等) String requestJson “将encryptedRequest转为JSON”; // ... 发送POST请求到 baseUrl apiPathContent-Type为application/json // 7. 收到响应后解密响应数据假设响应体也是EncryptedResponse结构 // String responseJson httpResponse.body().string(); // EncryptedResponse resp parse(responseJson); // if (resp.getCode() 200 StrUtil.isNotBlank(resp.getEncryptedData())) { // byte[] respCipherBytes Base64.decode(resp.getEncryptedData()); // // 分离IV和密文约定同上 // byte[] respIv ...; // byte[] respActualCipher ...; // AES respAes new AES(Mode.CBC, Padding.PKCS5Padding, sessionKey, respIv); // byte[] decryptedResp respAes.decrypt(respActualCipher); // String realRespJson new String(decryptedResp, StandardCharsets.UTF_8); // // 将realRespJson反序列化为目标对象 // } return “解密后的响应JSON”; } }客户端注意事项公钥管理客户端应缓存服务端公钥并定期如每天更新以应对服务端密钥轮转。AES模式选择务必使用带IV的模式如CBC、CTR切勿使用ECB模式。ECB模式相同的明文块会产生相同的密文块安全性极低。IV的生成每次加密都必须使用随机生成的IV并将IV与密文一起传输。IV不需要保密但必须是不可预测的。错误处理网络请求、加解密过程都可能出错需要有完善的异常处理和重试机制。6. 性能、安全考量与进阶优化一套方案不能只考虑功能还得掂量下性能和更深层次的安全。性能影响分析 主要的开销在RSA解密和AES加解密。RSA解密服务端用私钥解密encryptedKey是CPU密集型操作每次请求都要做。以2048位RSA为例单次解密在主流服务器上耗时约几毫秒。对于QPS几百的系统这个开销可以接受。如果QPS上万就需要考虑连接复用与会话密钥缓存可以为每个客户端连接或会话生成一个AES密钥并在一定时间内复用减少RSA解密次数。但这会降低前向安全性需要权衡。硬件加速使用支持RSA-NI指令集的CPU可以大幅提升RSA性能。异步解密将解密操作放到独立的线程池不阻塞业务线程。AES加解密速度很快通常不是瓶颈。安全进阶优化密钥轮转RSA密钥对应定期如每月更换。更换时新旧公钥需要有一段共存期让所有客户端平滑升级。签名验签在EncryptedRequest中增加sign字段。客户端用sessionKey对(encryptedKey encryptedData timestamp nonce)进行HMAC计算服务端用解密出的sessionKey重新计算并比对。这可以防止传输过程中数据被篡改虽然HTTPS也能防但提供了应用层额外保障。更完善的防重放使用noncetimestamp是基础方案。生产环境应将使用过的nonce存入分布式缓存如Redis并设置合理的过期时间略大于TIMESTAMP_TOLERANCE同时要防止缓存被恶意填满的DoS攻击。密钥长度与算法RSA至少2048位推荐3072位。AES至少128位推荐256位。确保使用的JCE策略文件支持无限强度加密。7. 常见问题排查与调试技巧在实际开发和联调中你肯定会遇到各种问题。这里列几个我们踩过的坑和解决方法。问题1服务端RSA解密失败报错“IllegalBlockSizeException: Data must not be longer than 256 bytes”。原因RSA有加密长度限制。2048位的密钥默认的PKCS#1填充方式下能加密的最大明文长度是245字节左右。你的AES密钥比如32字节加上填充信息后长度是符合的。这个错误通常意味着你传给RSA加密的数据太长了或者你错误地将整个请求体都用RSA加密了。解决确保只有AES会话密钥长度固定为16/24/32字节用RSA加密。业务数据必须用AES加密。问题2AES解密失败报错“Given final block not properly padded”。原因这是填充错误。可能的原因有客户端和服务端使用的AES模式或填充方式不一致如一个用CBC一个用ECB。IV处理不一致。客户端加密时用了IV但服务端解密时没有正确提取或使用相同的IV。加密数据的Base64编解码出错或者传输过程中密文被损坏。解决首先核对双方代码中的AES/Mode/Padding字符串是否完全一致。在客户端和服务端分别打印或日志记录关键步骤的中间结果生成的sessionKeyHex、IVHex、加密前的明文、加密后的密文Base64。对比服务端接收并Base64解码后的密文是否与客户端发送的一致。确认IV的传递机制。我们的方案是将IV拼接到密文前一起传输服务端需要按约定长度16字节拆分。问题3防重放校验总是失败。原因客户端和服务端系统时间不同步或者nonce生成逻辑有问题如重复了。解决确保客户端和服务端使用同步的、准确的时间源如NTP。检查nonce的生成是否足够随机建议使用UUID或安全的随机数生成器。检查服务端nonceCache的实现如果是内存Map注意应用重启后缓存会清空。如果是分布式缓存检查网络连接和序列化是否正确。问题4性能测试发现TPS上不去。原因RSA解密是瓶颈。解决使用JProfiler或Arthas等工具定位热点确认耗时确实在RSA.decrypt。考虑引入“会话密钥缓存”。在第一次成功解密后将(clientIp某个标识, sessionKey)缓存起来设置一个较短的过期时间如30秒。后续请求如果携带相同的encryptedKey可以加个标识字段则直接使用缓存的sessionKey跳过RSA解密。这需要在协议设计上增加一个可选的keyId或使用encryptedKey的哈希作为缓存键。这套从JWT“裸奔”升级到混合加密的方案在一年多的时间里为我们核心业务接口提供了坚实的安全保障。它确实引入了一些复杂性但相比于数据泄露可能带来的灾难性后果这些投入是绝对值得的。安全从来不是一个可以“做完”的功能而是一个持续的过程。这套方案为我们打下了良好的基础后续我们还在上面集成了更细粒度的审计日志和基于行为的异常检测。希望这份详细的拆解能帮助你构建出更安全的API系统。