Spring Boot 3.4接口安全实战:RSA+AES混合加密与签名验签方案

发布时间:2026/7/26 9:17:05
Spring Boot 3.4接口安全实战:RSA+AES混合加密与签名验签方案 1. 项目概述为什么我们需要在Spring Boot 3.4中实现接口加解密与防篡改最近在做一个金融类的项目对接的客户方在安全评审时明确提出了需要满足等保2.0中关于数据传输安全性的要求。核心就两条一是数据在传输过程中不能被窃听机密性二是数据在传输过程中不能被恶意修改完整性。这让我不得不重新审视我们基于Spring Boot 3.4构建的这套RESTful API。过去我们可能觉得用了HTTPS就万事大吉但深入想想HTTPS确实解决了传输层的安全可一旦请求到达我们的应用服务器数据就是明文了。如果我们的服务部署在云上或者内部网络存在风险那么从负载均衡器到应用服务、从应用到数据库这些环节的数据依然可能暴露。更关键的是HTTPS无法防止数据被篡改后重新签名虽然概率低但并非不可能。所以在应用层再做一层加解密和签名验签就成了一道必要的“内网安全门”。这个实战项目的目标很明确在Spring Boot 3.4应用中为关键业务接口比如支付、用户敏感信息查询实现一套完整的、非对称与对称加密结合、并带有防篡改签名的安全通信方案。简单说就是客户端用我们的RSA公钥加密一个随机生成的AES密钥然后用这个AES密钥加密真正的业务数据最后对加密后的整体数据做一个签名。服务端收到后先用自己的RSA私钥解密出AES密钥再用AES密钥解密业务数据最后验证签名是否一致。这套组合拳下来安全性就非常扎实了。它适合所有对数据安全有较高要求的Spring Boot开发者特别是涉及金融、政务、医疗、企业核心业务等场景。即使你现在没这个硬性要求提前掌握这套方案在技术面试或者架构设计时也是一个非常加分的亮点。2. 核心方案设计与密码学选型背后的逻辑为什么是RSAAES签名验签这个组合而不是直接用RSA加密所有数据或者只用AES这里面的门道我结合等保要求和实际性能仔细琢磨过。2.1 非对称加密RSA与对称加密AES的混合使用RSA算法非常安全但有个致命缺点慢。它不适合加密大量数据。如果我用RSA去加密一个几KB的JSON报文加解密过程会消耗可观的CPU和时间在高并发接口下这是不可接受的。而AES作为对称加密算法加解密速度极快非常适合处理大批量数据。所以一个经典的混合加密模式就诞生了用RSA来加密“钥匙”用AES来加密“货物”。具体到我们的流程客户端随机生成一个AES密钥比如256位的。这个密钥就是用来加密业务数据的“会话密钥”。客户端用服务端预先提供的RSA公钥加密这个AES密钥。因为AES密钥本身很短一个字符串用RSA加密很快。客户端用上一步生成的AES密钥采用AES算法如AES/CBC/PKCS5Padding加密实际的业务JSON数据。最后客户端将加密后的AES密钥RSA加密结果和加密后的业务数据AES加密结果一起发送给服务端。服务端收到后反向操作用自己的RSA私钥解密出AES密钥。用解密得到的AES密钥解密出原始的业务数据。这样我们既利用了RSA的非对称特性安全地交换了密钥又享受了AES加密大数据的高性能。这是目前最主流、最稳妥的实践。2.2 数字签名验签的必要性加解密保证了机密性但无法保证完整性。想象一个场景黑客截获了请求他虽然无法解密出原始数据因为没有私钥但他可以恶意篡改加密后的数据块哪怕只是改一个比特然后原样发送给服务端。服务端解密时可能会因为数据损坏而报错但这已经造成了服务拒绝。更糟糕的是如果黑客精心构造会不会有可能在不解密的情况下篡改数据并让服务端解密出他期望的结果虽然极其困难但密码学上要求我们考虑这种可能性数字签名就是为了解决这个问题。它的逻辑是发送方客户端对“待发送的数据”可以是明文也可以是密文计算一个哈希值如SHA256然后用自己的私钥对这个哈希值进行加密得到的就是“签名”。接收方服务端收到数据和签名后做两件事用发送方的公钥解密签名得到哈希值A。对自己收到的数据计算哈希值得到哈希值B。比较A和B。如果一致证明数据在传输过程中未被篡改且确实来自持有对应私钥的发送方。在我们的方案里通常是对“加密后的业务数据”进行签名。因为业务数据已经用AES加密了所以签名相当于对密文进行完整性保护。有些更严格的场景会对“明文业务数据”先签名然后再加密即“签名后加密”确保源头可信。我们这里采用对密文签名在性能和安全性上是一个很好的平衡。2.3 关键参数选型与等保考量等保2.0对密码技术有明确要求我们不能随便选个算法和长度就上。RSA密钥长度等保2.0三级及以上通常要求RSA密钥长度不低于2048位。我们直接选择2048位。生成密钥对时记得保存好私钥公钥可以提供给客户端。AES模式与填充AES有多种工作模式如ECB、CBC、GCM等。ECB模式不安全不推荐。CBC模式需要初始化向量IV安全性好是常用选择。GCM模式同时提供了加密和认证更先进但实现稍复杂。我们这里选择最通用的AES/CBC/PKCS5Padding。注意CBC模式每次加密必须使用一个随机生成的IV和加密后的数据一起传给服务端。哈希算法用于签名的哈希算法我们选择SHA256withRSA。SHA256是目前公认安全的哈希算法强度足够。密钥管理这是安全的核心RSA私钥必须存储在服务端安全的位置如配置文件需加密、环境变量或专业的密钥管理服务KMS中绝不能硬编码在代码里或提交到代码库。AES的“会话密钥”是每次请求动态生成的用完即弃安全性很高。注意等保测评时测评人员会重点关注密钥的生成、存储、分发、使用和销毁的全生命周期管理。一定要有相应的文档和日志记录。3. 环境准备与核心工具类封装工欲善其事必先利其器。在开始编写业务代码前我们需要先把加解密、签名验签这些底层能力封装成可靠的工具类。我基于Spring Boot 3.4和JDK 17进行开发。3.1 项目初始化与依赖引入创建一个新的Spring Boot 3.4项目选择Web依赖。在pom.xml中我们主要需要确保JDK版本并引入一些辅助工具。properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 用于处理JSON -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- 工具类如Base64编码JDK自带也可用 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies加解密和签名功能JDK的标准库javax.crypto和java.security已经足够强大不需要额外引入第三方密码学库如Bouncy Castle除非你有特别的需求如国密算法SM2/SM4。3.2 RSA密钥对生成与管理首先我们需要生成一对RSA密钥。这里我写一个工具类来生成并演示如何保存和加载。在实际生产环境中密钥对通常由运维或安全人员在安全环境中生成然后以文件或配置的形式提供给应用。import javax.crypto.Cipher; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class RsaUtil { private static final String RSA_ALGORITHM RSA; private static final int KEY_SIZE 2048; // 密钥长度 /** * 生成RSA密钥对 * return 包含公钥和私钥的KeyPair * throws NoSuchAlgorithmException */ public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator KeyPairGenerator.getInstance(RSA_ALGORITHM); keyPairGenerator.initialize(KEY_SIZE); return keyPairGenerator.generateKeyPair(); } /** * 获取公钥字符串Base64编码 */ public static String getPublicKeyStr(KeyPair keyPair) { return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); } /** * 获取私钥字符串Base64编码 */ public static String getPrivateKeyStr(KeyPair keyPair) { return Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); } /** * 从Base64字符串加载公钥 */ public static PublicKey loadPublicKey(String publicKeyStr) throws Exception { byte[] keyBytes Base64.getDecoder().decode(publicKeyStr); X509EncodedKeySpec keySpec new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA_ALGORITHM); return keyFactory.generatePublic(keySpec); } /** * 从Base64字符串加载私钥 */ public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { byte[] keyBytes Base64.getDecoder().decode(privateKeyStr); PKCS8EncodedKeySpec keySpec new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory KeyFactory.getInstance(RSA_ALGORITHM); return keyFactory.generatePrivate(keySpec); } /** * RSA公钥加密 * param data 待加密数据 * param publicKey 公钥 * return 加密后的Base64字符串 */ public static String encryptWithPublicKey(String data, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedBytes cipher.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(encryptedBytes); } /** * RSA私钥解密 * param encryptedData 加密后的Base64字符串 * param privateKey 私钥 * return 解密后的原始字符串 */ public static String decryptWithPrivateKey(String encryptedData, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] dataBytes Base64.getDecoder().decode(encryptedData); byte[] decryptedBytes cipher.doFinal(dataBytes); return new String(decryptedBytes); } }实操心得Cipher.getInstance(“RSA”)默认使用的是RSA/ECB/PKCS1Padding模式。对于仅加密密钥这种短数据这是安全的。如果你需要加密的数据块长度接近密钥长度则需要考虑分块加密。我们的场景只加密AES密钥一个固定长度的字符串所以直接用这个默认模式没问题。3.3 AES加解密工具类封装接下来封装AES工具类。重点在于CBC模式下的IV处理。import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class AesUtil { private static final String AES_ALGORITHM AES; private static final String AES_TRANSFORMATION AES/CBC/PKCS5Padding; // 使用CBC模式 private static final int AES_KEY_SIZE 256; // AES-256 /** * 生成一个随机的AES密钥 */ public static SecretKey generateAesKey() throws NoSuchAlgorithmException { KeyGenerator keyGenerator KeyGenerator.getInstance(AES_ALGORITHM); keyGenerator.init(AES_KEY_SIZE); return keyGenerator.generateKey(); } /** * 生成一个随机的16字节初始化向量(IV) */ public static byte[] generateIv() { byte[] iv new byte[16]; // AES块大小是16字节 new SecureRandom().nextBytes(iv); return iv; } /** * AES加密 * param data 明文数据 * param secretKey AES密钥 * param iv 初始化向量 * return 加密后的Base64字符串 */ public static String encrypt(String data, SecretKey secretKey, byte[] iv) throws Exception { Cipher cipher Cipher.getInstance(AES_TRANSFORMATION); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] encryptedBytes cipher.doFinal(data.getBytes()); // 将IV和加密后的数据一起返回IV不需要保密但必须唯一且随机 byte[] combined new byte[iv.length encryptedBytes.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length); return Base64.getEncoder().encodeToString(combined); } /** * AES解密 * param combinedData 包含IV和密文的Base64字符串 * param secretKey AES密钥 * return 解密后的明文 */ public static String decrypt(String combinedData, SecretKey secretKey) throws Exception { byte[] combined Base64.getDecoder().decode(combinedData); byte[] iv new byte[16]; byte[] encryptedBytes new byte[combined.length - 16]; System.arraycopy(combined, 0, iv, 0, 16); System.arraycopy(combined, 16, encryptedBytes, 0, encryptedBytes.length); Cipher cipher Cipher.getInstance(AES_TRANSFORMATION); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] decryptedBytes cipher.doFinal(encryptedBytes); return new String(decryptedBytes); } /** * 将Base64编码的密钥字符串转换为SecretKey对象 */ public static SecretKey convertStringToSecretKey(String encodedKey) { byte[] decodedKey Base64.getDecoder().decode(encodedKey); return new SecretKeySpec(decodedKey, 0, decodedKey.length, AES_ALGORITHM); } /** * 将SecretKey对象转换为Base64编码的字符串 */ public static String convertSecretKeyToString(SecretKey secretKey) { return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } }3.4 签名与验签工具类封装最后我们封装签名和验签的工具。这里我们使用SHA256作为哈希算法RSA进行签名。import java.security.*; import java.util.Base64; public class SignatureUtil { private static final String SIGNATURE_ALGORITHM SHA256withRSA; /** * 使用私钥对数据进行签名 * param data 待签名数据通常是密文 * param privateKey 签名私钥 * return 签名的Base64字符串 */ public static String sign(String data, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SIGNATURE_ALGORITHM); signature.initSign(privateKey); signature.update(data.getBytes()); byte[] signBytes signature.sign(); return Base64.getEncoder().encodeToString(signBytes); } /** * 使用公钥验证签名 * param data 接收到的数据应与签名时的数据一致 * param sign 接收到的签名 * param publicKey 验证公钥 * return 验签是否通过 */ public static boolean verify(String data, String sign, PublicKey publicKey) throws Exception { Signature signature Signature.getInstance(SIGNATURE_ALGORITHM); signature.initVerify(publicKey); signature.update(data.getBytes()); byte[] signBytes Base64.getDecoder().decode(sign); return signature.verify(signBytes); } }至此我们的核心密码学工具就准备好了。接下来就是如何将这些工具优雅地集成到Spring Boot的请求/响应流程中。4. Spring Boot接口的集成实战请求解密与响应加密我们不希望在每个Controller里都写一堆加解密的代码那样太冗余且容易出错。Spring Boot的拦截器Interceptor和ControllerAdvice是处理这类横切关注点的绝佳武器。我的设计思路是定义一个注解如SecurityApi标记在需要安全处理的Controller方法上。编写一个拦截器对带有SecurityApi的请求进行解密和验签。使用ControllerAdvice和ResponseBodyAdvice对带有SecurityApi的响应进行加密和签名。4.1 定义安全注解与加解密请求/响应体首先定义注解和用于传输加密数据的请求/响应体结构。// 安全API注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface SecurityApi { // 可以扩展比如指定是否需要验签等 }// 加密请求体 Data public class EncryptedRequest { /** * 经过RSA公钥加密后的AES密钥Base64格式 */ private String encryptedKey; /** * 经过上述AES密钥加密后的业务数据Base64格式 */ private String encryptedData; /** * 对encryptedData的签名Base64格式 */ private String sign; // 其他可能的信息如时间戳、随机数等用于防重放 private String timestamp; private String nonce; }// 加密响应体 Data public class EncryptedResponse { /** * 状态码 */ private int code; /** * 经过RSA公钥加密后的AES密钥Base64格式 * 注意响应时服务端会生成新的AES会话密钥 */ private String encryptedKey; /** * 经过新AES密钥加密后的业务响应数据Base64格式 */ private String encryptedData; /** * 对encryptedData的签名Base64格式 */ private String sign; }4.2 构建安全拦截器请求解密与验签这是核心环节。拦截器需要判断当前请求方法是否有SecurityApi注解。读取请求体解析为EncryptedRequest对象。用服务端的RSA私钥解密encryptedKey得到AES会话密钥。用解密出的AES密钥解密encryptedData得到业务JSON字符串。使用客户端的公钥需要预先配置或从请求头获取验证sign签名。验签通过后将解密后的业务JSON字符串通过Jackson反序列化成Controller方法实际需要的参数对象。将参数对象设置到请求属性中供后续处理。这里有个关键点Spring的HttpServletRequest的输入流只能读取一次。我们需要自定义一个HttpServletRequestWrapper来缓存请求体。Component public class SecurityInterceptor implements HandlerInterceptor { Autowired private RsaKeyProvider rsaKeyProvider; // 一个提供RSA密钥的服务 Autowired private ObjectMapper objectMapper; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; SecurityApi securityApi handlerMethod.getMethodAnnotation(SecurityApi.class); if (securityApi null) { return true; // 没有注解放行 } // 1. 读取并解析加密请求体 String requestBody getRequestBody(request); EncryptedRequest encryptedRequest objectMapper.readValue(requestBody, EncryptedRequest.class); // 2. RSA解密获取AES密钥 String aesKeyStr RsaUtil.decryptWithPrivateKey(encryptedRequest.getEncryptedKey(), rsaKeyProvider.getServerPrivateKey()); SecretKey aesKey AesUtil.convertStringToSecretKey(aesKeyStr); // 3. AES解密获取业务数据明文 String businessDataPlain AesUtil.decrypt(encryptedRequest.getEncryptedData(), aesKey); // 4. 验签这里假设客户端公钥已配置实际可能根据appId从数据库或缓存获取 PublicKey clientPublicKey rsaKeyProvider.getClientPublicKey(defaultClientId); boolean verifyResult SignatureUtil.verify(encryptedRequest.getEncryptedData(), encryptedRequest.getSign(), clientPublicKey); if (!verifyResult) { throw new SecurityException(签名验证失败请求可能被篡改); } // 5. 防重放攻击检查可选但重要 // checkTimestampAndNonce(encryptedRequest.getTimestamp(), encryptedRequest.getNonce()); // 6. 将解密后的业务数据绑定到请求参数 // 这里需要知道Controller方法参数的类型简单起见我们假设是String或一个特定的DTO // 更复杂的实现需要解析HandlerMethod的参数信息 Class?[] parameterTypes handlerMethod.getMethod().getParameterTypes(); if (parameterTypes.length 1 parameterTypes[0].equals(String.class)) { request.setAttribute(DECRYPTED_BODY, businessDataPlain); } else { // 尝试反序列化为第一个参数的类型 Object arg objectMapper.readValue(businessDataPlain, parameterTypes[0]); request.setAttribute(DECRYPTED_BODY, arg); } // 7. 将AES密钥存入请求上下文供后续加密响应使用注意请求和响应的AES密钥应不同这里仅为示例流程 request.setAttribute(SESSION_AES_KEY, aesKey); return true; } private String getRequestBody(HttpServletRequest request) throws IOException { // 使用缓存请求体的Wrapper if (request instanceof CachedBodyHttpServletRequest) { return ((CachedBodyHttpServletRequest) request).getBody(); } // 如果不是则读取流这种情况应该不会发生因为需要在Filter中包装 return StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); } // 省略 CachedBodyHttpServletRequest 的实现... }为了让拦截器能读取多次请求体我们还需要一个Filter来包装Request。Component public class CachingRequestBodyFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只对特定路径或内容类型的请求进行包装避免性能损耗 if (isSecurityRequest(request)) { CachedBodyHttpServletRequest cachedBodyRequest new CachedBodyHttpServletRequest(request); filterChain.doFilter(cachedBodyRequest, response); } else { filterChain.doFilter(request, response); } } private boolean isSecurityRequest(HttpServletRequest request) { // 根据路径或Header判断是否为需要解密的请求 return true; // 简化实现 } }4.3 实现响应加密与签名ResponseBodyAdvice请求处理完后我们需要对返回的结果进行加密和签名。这里用ControllerAdvice实现ResponseBodyAdvice。ControllerAdvice(annotations SecurityApi.class) // 只处理带有SecurityApi注解的方法 public class EncryptResponseBodyAdvice implements ResponseBodyAdviceObject { Autowired private RsaKeyProvider rsaKeyProvider; Autowired private ObjectMapper objectMapper; Override public boolean supports(MethodParameter returnType, Class converterType) { // 检查返回类型通常我们只加密返回对象不加密String或void等 return returnType.getMethod().isAnnotationPresent(SecurityApi.class); } Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { try { // 1. 生成新的AES会话密钥和IV每次响应都应不同 SecretKey aesKey AesUtil.generateAesKey(); byte[] iv AesUtil.generateIv(); // 2. 将业务响应体序列化为JSON字符串 String businessDataJson objectMapper.writeValueAsString(body); // 3. 使用新的AES密钥加密业务数据 String encryptedData AesUtil.encrypt(businessDataJson, aesKey, iv); // 4. 使用客户端的RSA公钥加密AES密钥 PublicKey clientPublicKey rsaKeyProvider.getClientPublicKey(defaultClientId); String encryptedKey RsaUtil.encryptWithPublicKey(AesUtil.convertSecretKeyToString(aesKey), clientPublicKey); // 5. 使用服务端私钥对加密后的数据签名 PrivateKey serverPrivateKey rsaKeyProvider.getServerPrivateKey(); String sign SignatureUtil.sign(encryptedData, serverPrivateKey); // 6. 构造加密响应体 EncryptedResponse encryptedResponse new EncryptedResponse(); encryptedResponse.setCode(200); encryptedResponse.setEncryptedKey(encryptedKey); encryptedResponse.setEncryptedData(encryptedData); encryptedResponse.setSign(sign); // 7. 设置响应头告知客户端内容类型 response.getHeaders().setContentType(MediaType.APPLICATION_JSON); return encryptedResponse; } catch (Exception e) { throw new RuntimeException(响应加密失败, e); } } }4.4 密钥提供器与配置管理我们需要一个中心化的地方来管理RSA密钥对。这里用ConfigurationProperties和Component实现一个简单的密钥提供器。生产环境务必使用更安全的方式如从KMS获取。Component ConfigurationProperties(prefix security.rsa) Data public class RsaKeyProvider { /** * 服务端私钥Base64编码 */ private String serverPrivateKeyStr; /** * 客户端公钥Base64编码可配置多个这里简化为一个 */ private String clientPublicKeyStr; private PrivateKey serverPrivateKey; private PublicKey clientPublicKey; PostConstruct public void init() throws Exception { this.serverPrivateKey RsaUtil.loadPrivateKey(serverPrivateKeyStr); this.clientPublicKey RsaUtil.loadPublicKey(clientPublicKeyStr); } public PrivateKey getServerPrivateKey() { return serverPrivateKey; } public PublicKey getClientPublicKey(String clientId) { // 简化处理实际应根据clientId返回对应的公钥 return clientPublicKey; } }在application.yml中配置security: rsa: server-private-key-str: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO... (你的私钥Base64) client-public-key-str: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD... (客户端公钥Base64)4.5 编写测试Controller最后我们写一个简单的Controller来测试整个流程。RestController RequestMapping(/api/secure) public class SecureDemoController { PostMapping(/payment) SecurityApi // 标记此接口需要安全处理 public ApiResponsePaymentResult makePayment(RequestBody PaymentRequest paymentRequest) { // 这里的paymentRequest已经是拦截器解密并反序列化好的对象了 System.out.println(接收到支付请求: paymentRequest); // 处理业务逻辑... PaymentResult result new PaymentResult(); result.setOrderId(ORDER_123456); result.setStatus(SUCCESS); return ApiResponse.success(result); // ResponseBodyAdvice会自动将这个ApiResponse对象加密后返回 } Data static class PaymentRequest { private String orderNo; private BigDecimal amount; private String userId; } Data static class PaymentResult { private String orderId; private String status; } Data static class ApiResponseT { private int code; private String msg; private T data; public static T ApiResponseT success(T data) { ApiResponseT response new ApiResponse(); response.setCode(200); response.setMsg(成功); response.setData(data); return response; } } }这样一个完整的、集成到Spring Boot的接口加解密与防篡改方案就搭建起来了。客户端需要按照我们定义的EncryptedRequest格式组装数据服务端会自动处理加解密和验签。5. 客户端调用示例与联调要点服务端准备好了客户端可能是前端、安卓、iOS或其他后端服务该如何调用呢这里给出一个Java客户端的示例其他语言原理相同。5.1 客户端加密与签名流程public class SecurityClient { private String serverPublicKeyStr; // 服务端公钥 private String clientPrivateKeyStr; // 客户端私钥用于签名 private PublicKey serverPublicKey; private PrivateKey clientPrivateKey; private ObjectMapper objectMapper new ObjectMapper(); public void init() throws Exception { serverPublicKey RsaUtil.loadPublicKey(serverPublicKeyStr); clientPrivateKey RsaUtil.loadPrivateKey(clientPrivateKeyStr); } public EncryptedRequest buildEncryptedRequest(Object businessData) throws Exception { // 1. 生成随机的AES密钥和IV SecretKey aesKey AesUtil.generateAesKey(); byte[] iv AesUtil.generateIv(); String aesKeyStr AesUtil.convertSecretKeyToString(aesKey); // 2. 将业务数据转为JSON String businessDataJson objectMapper.writeValueAsString(businessData); // 3. AES加密业务数据 String encryptedData AesUtil.encrypt(businessDataJson, aesKey, iv); // 4. RSA加密AES密钥 String encryptedKey RsaUtil.encryptWithPublicKey(aesKeyStr, serverPublicKey); // 5. 使用客户端私钥对 encryptedData 签名 String sign SignatureUtil.sign(encryptedData, clientPrivateKey); // 6. 构造请求体 EncryptedRequest request new EncryptedRequest(); request.setEncryptedKey(encryptedKey); request.setEncryptedData(encryptedData); request.setSign(sign); request.setTimestamp(String.valueOf(System.currentTimeMillis())); request.setNonce(UUID.randomUUID().toString()); // 随机数防重放 return request; } public T T parseEncryptedResponse(EncryptedResponse encryptedResponse, ClassT clazz) throws Exception { // 1. 验证服务端签名使用服务端公钥 boolean verify SignatureUtil.verify(encryptedResponse.getEncryptedData(), encryptedResponse.getSign(), serverPublicKey); if (!verify) { throw new SecurityException(服务端响应签名验证失败); } // 2. RSA解密获取响应使用的AES密钥 String aesKeyStr RsaUtil.decryptWithPrivateKey(encryptedResponse.getEncryptedKey(), clientPrivateKey); // 注意这里用客户端私钥解密 SecretKey aesKey AesUtil.convertStringToSecretKey(aesKeyStr); // 3. AES解密业务数据 String businessDataJson AesUtil.decrypt(encryptedResponse.getEncryptedData(), aesKey); // 4. 反序列化 return objectMapper.readValue(businessDataJson, clazz); } }5.2 使用HTTP客户端发送请求public class ApiClient { private SecurityClient securityClient; private RestTemplate restTemplate; public PaymentResult callSecurePayment(PaymentRequest request) throws Exception { // 1. 构建加密请求体 EncryptedRequest encryptedRequest securityClient.buildEncryptedRequest(request); // 2. 发送HTTP请求 HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityEncryptedRequest httpEntity new HttpEntity(encryptedRequest, headers); ResponseEntityEncryptedResponse responseEntity restTemplate.postForEntity( https://your-server.com/api/secure/payment, httpEntity, EncryptedResponse.class ); // 3. 处理加密响应 EncryptedResponse encryptedResponse responseEntity.getBody(); if (encryptedResponse.getCode() ! 200) { throw new RuntimeException(API调用失败: encryptedResponse.getCode()); } // 4. 解密并解析响应 ApiResponsePaymentResult apiResponse securityClient.parseEncryptedResponse(encryptedResponse, objectMapper.getTypeFactory().constructParametricType(ApiResponse.class, PaymentResult.class)); return apiResponse.getData(); } }5.3 联调过程中的关键检查点在实际联调时最容易出问题的地方往往是一些细节密钥格式确保客户端和服务端使用的密钥是同一对公私钥匹配并且是Base64编码的PKCS#8格式对于私钥和X.509格式对于公钥。用openssl或在线工具生成密钥时要注意格式转换。编码一致性加解密和签名验签过程中涉及字符串和字节数组转换的地方必须统一编码通常用UTF-8。data.getBytes()最好显式指定为data.getBytes(StandardCharsets.UTF_8)。AES的IV处理CBC模式必须使用IV且每次加密必须随机生成。服务端解密时必须从combinedData的前16字节正确提取IV。签名内容确保客户端和服务端对“哪些数据做签名”达成一致。我们这里是对encryptedDataAES密文签名。如果一方对明文签名另一方对密文验签肯定失败。HTTP请求头确保客户端发送的Content-Type是application/json服务端能正确解析。时间戳与防重放在生产环境中务必在拦截器里实现防重放攻击机制。可以检查请求中的timestamp如果与服务器时间相差过大如5分钟则拒绝。同时将nonce存入缓存如Redis在一段时间内如5分钟重复的nonce请求也拒绝。6. 生产环境进阶考量与性能优化把demo跑起来只是第一步要真正上线还有很多坑要填。6.1 密钥的安全存储与轮转存储绝不能把私钥放在代码或配置文件中明文提交到Git。推荐做法使用环境变量注入。使用云服务商的密钥管理服务如AWS KMS, Azure Key Vault, 阿里云KMS。在启动时从安全的配置中心如Nacos, Apollo拉取配置中心本身需加密。轮转密钥不能永远不换。需要制定密钥轮转策略。例如每季度更换一次RSA密钥对。轮转期间新旧密钥需要同时支持一段时间逐步迁移客户端。6.2 性能优化策略全量加解密对CPU有一定消耗特别是RSA运算。优化思路选择性加密并非所有接口都需要如此高的安全级别。使用SecurityApi注解可以灵活控制。对于不敏感的数据如查询公开信息可以不走加解密流程。连接复用与会话密钥对于WebSocket或需要频繁交互的接口可以在首次握手时协商一个AES会话密钥并在本次会话内复用避免每次请求都进行RSA加解密。但需要妥善管理会话密钥的生命周期。异步处理加解密是CPU密集型操作可以考虑将加解密操作放到独立的线程池中执行避免阻塞Netty或Tomcat的IO线程。硬件加速如果性能压力极大可以考虑使用支持AES-NI指令集的CPUJava会自动利用其进行硬件加速。6.3 监控、日志与审计监控监控加解密微服务的CPU使用率、接口响应时间特别是解密和验签耗时。日志谨慎记录日志。绝对不要在日志中打印明文敏感信息如解密后的银行卡号、密码、完整的密钥或IV。可以记录一些元信息如请求ID、客户端ID、加解密操作的成功/失败状态、签名验证结果等。审计记录所有安全相关操作特别是验签失败、解密失败、重放攻击拦截等事件便于事后追溯和安全分析。6.4 常见问题排查清单在实际部署和运维中你可能会遇到下面这些问题问题现象可能原因排查步骤服务端解密失败javax.crypto.BadPaddingException1. 客户端使用的RSA公钥与服务端私钥不匹配。2. 加密的数据块长度超过了RSA密钥能处理的最大长度。3. 传输过程中encryptedKey字符串被修改如空格、换行符。1. 核对双方密钥对是否匹配。2. 确认RSA只用于加密AES密钥短字符串。3. 检查Base64字符串的编码/解码是否一致网络传输是否有URL编码问题。服务端AES解密失败javax.crypto.IllegalBlockSizeException1. AES密钥错误。2. IV提取错误长度或位置不对。3. 密文在传输中被损坏。1. 确认RSA解密AES密钥步骤成功。2. 调试检查combinedData拆分IV和密文的逻辑。3. 对比客户端发送的encryptedData和服务端收到的是否完全一致。签名验证失败1. 签名算法不一致如一端用SHA256另一端用SHA1。2. 签名的原始数据不一致客户端对A签名服务端对B验签。3. 用于验签的公钥与签名的私钥不匹配。4. 签名字符串在传输中有变化。1. 确认双方使用的签名算法字符串完全相同SHA256withRSA。2. 在客户端和服务端分别打印出用于签名/验签的encryptedData的Base64字符串进行比对。3. 核对公私钥对应关系。接口响应明显变慢1. RSA加解密成为性能瓶颈。2. 密钥加载或转换耗时。1. 使用性能分析工具如Arthas定位热点。2. 确保RSA密钥对象PrivateKey/PublicKey是单例复用而不是每次请求都重新加载。特定客户端一直失败1. 该客户端使用了错误的公钥或算法参数。2. 客户端实现有bug如IV生成逻辑错误。1. 为该客户端单独提供一份密钥和测试程序进行隔离调试。2. 检查客户端代码特别是字节数组与字符串转换、Base64编码等环节。这套方案从设计到实现涵盖了等保2.0中关于数据传输安全的核心要求。它不是一个银弹需要你根据自己业务的实际情况进行调整比如密钥管理方式、是否引入国密算法、如何与现有的网关和认证体系结合等。但它的核心思想——混合加密确保机密性、数字签名确保完整性——是构建安全通信的坚实基石。在实际踩坑和调优的过程中你会对应用层安全有更深刻的理解。