首页 / 资讯中心 / 文章详情

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

Spring Boot接口全链路加解密:RSA+AES防篡改防重放实战 ★ FEATURED ARTICLE
接口安全这件事很多团队的第一反应是“上HTTPS就够了”。HTTPS确实解决了传输链路加密但等你真正被安全测试报告点了名或者开放接口给外部合作方之后大概率会发现服务端收到请求时数据已经被还原成了明文之后的日志打印、网关转发、异常堆栈每一处都可能留下原文记录。说白了HTTPS只是把风险挡在了门口屋子里面依然到处都是裸奔的数据。我这次想聊的是在 Spring Boot 里用 RSA AES 做接口全链路加解密并把防篡改、防重放一起设计进去的完整方案。这套思路适用于对外开放 API、涉及资金或隐私数据、需要对核心接口做强控制的场景。后端开发可以直接从里面抄走一整套可运行的代码骨架甚至包括密钥管理、灰度开关和排障经验不只是给你几个“看起来能用”的工具类。1. 为什么需要全链路加解密HTTPS之外的真实威胁先别急着写代码把问题想透再做方案才不会拧巴。很多人对“接口加密”的理解是客户端把参数加密传给服务端服务端解密正常处理。这没错但它只是“局部加密”。全链路加解密强调的是从客户端发起请求到服务端返回响应整个数据进出边界的过程中业务明文不应该出现在任何中间环节。1.1 HTTPS之外的真实威胁HTTPS 保护的是“客户端 - 服务端”之间这段网络传输链路数据到达服务端之后就会被 TLS 终止还原成明文。之后数据的走向通常是这样应用网关或反向代理收到明文请求可能打访问日志业务代码在处理前打 debug 日志把请求体顺手打印出来服务端调用下游服务时如果用了 HTTP 明文协议内网里又是一段裸奔链路全局异常处理器捕获异常时常常把“请求参数”一并拼到告警信息里。这些环节里任何一处被拖库、被脱库、被内部人员访问核心字段手机号、身份证、支付金额就会直接暴露。更常见的是合作方场景你对接的外部系统拿着你的接口文档数据到了对方服务端之后对方的日志、监控、第三方插件一样会看到这些明文。你根本无法控制对方内部的数据处理流程。全链路加解密的价值在于把明文的出现范围压缩到最小只在业务代码的“解密之后”到“加密之前”这段内存里存在明文。1.2 全链路加解密到底保护了什么做全链路加解密核心解决四类问题传输链路上的明文暴露即使中间有旁路抓包拿到的是密文服务端日志、网关日志里的敏感信息泄露密文日志即使被打开也读不出业务含义数据在合作方/中间服务商侧二次转发时的泄露对方拿到的是密文和密钥密文密钥不落地报文被篡改或重放配合签名和防重放机制可以挡住绝大部分自动化脚本攻击。很多人担心性能。以现代机器的处理能力一次 RSA 解密大约 15 毫秒AES 解密一次申请几十微秒到几百微秒对于一个普通业务接口来说这个损耗在可接受范围内。后面我会专门讲性能实测和优化策略。2. RSA AES 混合加密方案选型与设计思路2.1 为什么不用纯 RSA 或纯 AES如果只用 RSA 做全量数据加密有两个硬伤性能差公钥加密、私钥解密涉及大数模幂运算比 AES 慢几个数量级。如果每个请求体都是几十上百 KBRSA 加密解密会吃掉大量 CPU明文长度限制RSA 一次能加密的明文长度取决于密钥长度。2048 位密钥配合 PKCS1Padding最多只能加密 245 字节。你可以把长文本分段加密但分段后密文膨胀、传输包变大代码复杂度也上去了。如果只用 AES问题同样明显AES 是对称加密加密和解密用同一把密钥密钥怎么安全地告诉对方直接把 AES 密钥放在请求里等于把保险柜钥匙贴在保险柜门上。所以常规做法是混合加密用 AES 加密业务数据用 RSA 保护 AES 密钥。AES 加解密速度快、没有长度限制RSA 只处理一把很小的密钥性能压力可以忽略。打个比方你要寄一箱贵重物品先把物品放进一个坚固的公文箱AES 加密再用一把保险柜钥匙去锁住公文箱的钥匙RSA 加密最后把公文箱和锁好的钥匙一起寄出去。收件人先用自己手上的保险柜钥匙打开外层拿到公文箱钥匙再打开公文箱取货。对比一下就清楚了方案性能密钥长度限制密钥管理适用场景全量 RSA慢CPU 开销大有需分段私钥服务端公钥客户端简单超短报文、低频场景全量 AES快无密钥分发困难局域网/内部服务间固定密钥场景RSA AES 混合快RSA 只处理小数据无AES 不限长公钥分发给客户端私钥服务端持有对外接口、互联网应用最常用2.2 一次完整请求的加解密流程先约定请求报文结构。客户端传给服务端的 JSON 长这样{ encryptKey: Base64(RSA加密后的AES密钥), iv: Base64(16字节随机IV), timestamp: 1735660800, nonce: 7f4a72c3-6d10-4a8e-b7d9-9f023c1d2e8b, sign: Base64(HMAC-SHA256签名), data: Base64(AES加密后的业务明文) }这里data是真正的业务参数密文比如{username:tom,amount:100}encryptKey是这次请求随机生成的 AES 密钥再用服务端 RSA 公钥加密iv是 AES 加密用的随机向量timestamp和nonce配合做防重放sign是消息认证码防篡改。服务端收到请求后的处理顺序是固定的先校验timestamp判断请求是否在可接受的时间窗口内再校验nonce判断这个随机数是不是已经被用过用 RSA 私钥解密encryptKey拿到本次请求的 AES 密钥用 AES 密钥校验sign确认data、timestamp、nonce没有被篡改用 AES 密钥和iv解密data得到业务明文继续走正常的 Controller 流程。注意顺序时间戳和 nonce 校验必须在 RSA 解密之前吗时间戳窗口校验可以在解密前先做nonce 也可以先查但验签必须在拿到 AES 密钥之后。所以实际代码里我会把前两步放在前面尽早拦截无效请求减少不必要的 RSA 解密开销。响应的结构保持一致。服务端返回时如果请求里已经有 AES 密钥就直接复用如果没有比如某些特殊场景就重新生成一个用客户端公钥加密后放回encryptKey字段。这样客户端只需要维护一次会话密钥响应解密不需要再做 RSA。2.3 防篡改签名与验签设计防篡改的本质是完整性校验。数据在传输过程中可能被中间人掉包必须确保收到的密文确实是客户端发的。常见做法有两种方案一HMAC-SHA256用 AES 密钥作为 MAC 密钥。计算签名的输入是data timestamp nonce密钥就是本次会话的 AES 密钥字节。AES 密钥只有客户端和服务端知道第三方既拿不到密钥也无法伪造签名。这是日常接口场景下我最推荐的做法实现简单、性能好。方案二SHA256withRSA客户端用私钥签名服务端用公钥验签。这种方案在 B 端对 B 端的场景更合适因为私钥签名具有不可抵赖性客户端无法否认“这笔请求是我发的”相当于法律意义上的电子签名。但缺点是私钥必须妥善保存在客户端环境一旦泄露后果严重。如果你们有合规审计要求可以选用这个方案普通 C 端接口HMAC 已经足够。签名输入为什么要带上timestamp和nonce因为如果只对data签名中间人可以替换掉时间戳和 nonce绕过防重放逻辑。把三个字段都纳入签名范围谁动任何一个字段验签都会失败逻辑才是闭环的。2.4 防重放时间戳 nonce 双保险防重放是防止攻击者把抓到的合法请求反复提交。单靠一个机制不够时间戳和 nonce 必须配合使用。时间戳窗口服务端判断Math.abs(当前时间 - timestamp) 窗口值就拒绝。窗口一般设置 3 到 5 分钟太短容易出现客户端时钟偏差导致误杀太长又会给重放留下更大的窗口。这里的本质是“过期请求一律不认”。nonce 幂等每个请求生成一个全局唯一的随机数UUID 或雪花 ID服务端把用过的 nonce 缓存起来在一定时间内重复出现的 nonce 直接拒绝。用 Redis 的SET NX EX实现最方便Boolean success redisTemplate.opsForValue() .setIfAbsent(crypto:nonce: nonce, 1, Duration.ofSeconds(500));返回 true 说明这个 nonce 第一次出现放行返回 false 说明有人拿相同 nonce 来过拒绝。时间戳能挡住窗口之外的旧请求nonce 能挡住窗口内的即时重放。比如攻击者 1 秒内重放 100 次同一个请求每个请求的 timestamp 都在窗口内如果没有 nonce 缓存这 100 次请求会全部被放过。只有两个机制配合才能既防“过期重放”又防“即时重放”。3. Spring Boot 实战落地加密能力接入理论说完了下面进入代码。我这边实现的思路是用注解声明加密范围用 RequestBodyAdvice 和 ResponseBodyAdvice 做自动解密和加密业务代码完全不感知加解密过程。3.1 基础工具箱AES 与 RSA 工具类实现先写 AES 工具类。我不会用 ECB 模式因为 ECB 相同明文会得到相同密文存在模式泄露风险实际项目中强烈建议用 CBC 模式加随机 IVpublic class AesUtil { private static final String ALGORITHM AES/CBC/PKCS5Padding; private static final int IV_SIZE 16; public static String encrypt(String plainText, byte[] aesKey, byte[] iv) { try { Cipher cipher Cipher.getInstance(ALGORITHM); SecretKeySpec keySpec new SecretKeySpec(aesKey, AES); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { throw new CryptoException(AES加密失败, e); } } public static String decrypt(String cipherText, byte[] aesKey, byte[] iv) { try { Cipher cipher Cipher.getInstance(ALGORITHM); SecretKeySpec keySpec new SecretKeySpec(aesKey, AES); IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, StandardCharsets.UTF_8); } catch (Exception e) { throw new CryptoException(AES解密失败, e); } } public static byte[] generateIv() { byte[] iv new byte[IV_SIZE]; new SecureRandom().nextBytes(iv); return iv; } public static byte[] generateKey() { byte[] key new byte[32]; // AES-256 new SecureRandom().nextBytes(key); return key; } }几个细节说明一下。AES 密钥我直接生成 32 字节即 AES-256JDK 8 update 161 之后默认支持不需要额外装扩展包。CBC 模式的 IV 必须是 16 字节随机生成随密文一起传给服务端。IV 本身不需要保密但不能重复使用同一个 IV 加密两个不同明文会泄露模式信息所以每个请求必须重新生成。再看 RSA 工具类public class RsaUtil { private static final String ALGORITHM RSA/ECB/PKCS1Padding; public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(data); } public static byte[] decrypt(byte[] data, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(data); } }这里要提一个 2048 位 RSA 的常识密钥长度 2048 位 256 字节PKCS1Padding 会占 11 字节所以一次最多加密 245 字节。我们的 AES 密钥只有 32 字节完全放得下根本不需要做分段处理。3.2 请求自动解密RequestBodyAdvice 实现解密逻辑放在哪里是个关键选择。如果用 Filter 读 body读完之后 Spring 的 Controller 就拿不到流了还得用 ContentCachingRequestWrapper 做包装繁琐且容易踩坑。我推荐直接用RequestBodyAdvice它是 Spring 提供给请求体反序列化前做拦截的扩展点可以把解密后的明文重新包装成 HttpInputMessage交给正常的 JSON 转换器。先定义注解用来标记哪些接口需要走加解密Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface EnableCrypto { } Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface IgnoreCrypto { }然后写核心的解密 AdviceRestControllerAdvice public class CryptoRequestAdvice implements RequestBodyAdvice { Resource private NonceManager nonceManager; Resource private CryptoProperties properties; Override public boolean supports(MethodParameter methodParameter, Type targetType, Class? extends HttpMessageConverter? converterType) { Class? clazz methodParameter.getContainingClass(); if (!clazz.isAnnotationPresent(RestController.class) !clazz.isAnnotationPresent(Controller.class)) { return false; } if (clazz.isAnnotationPresent(IgnoreCrypto.class) || methodParameter.hasMethodAnnotation(IgnoreCrypto.class)) { return false; } return clazz.isAnnotationPresent(EnableCrypto.class) || methodParameter.hasMethodAnnotation(EnableCrypto.class); } Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class? extends HttpMessageConverter? converterType) throws IOException { byte[] body StreamUtils.copyToByteArray(inputMessage.getBody()); if (body.length 0) { return inputMessage; } CryptoRequestDTO dto JSON.parseObject(body, CryptoRequestDTO.class); // 1. 防重放时间戳 nonce ReplayAttackChecker.check(dto.getTimestamp(), dto.getNonce(), nonceManager, properties); // 2. RSA 私钥解密获得本次请求的 AES 密钥 PrivateKey privateKey RsaKeyHolder.getPrivateKey(); byte[] aesKey RsaUtil.decrypt( Base64.getDecoder().decode(dto.getEncryptKey()), privateKey); // 3. HMAC 验签防篡改 boolean valid SignUtil.verify( dto.getData(), dto.getTimestamp(), dto.getNonce(), dto.getSign(), aesKey); if (!valid) { throw new CryptoException(签名校验失败); } // 4. AES 解密业务数据 byte[] iv Base64.getDecoder().decode(dto.getIv()); String plainText AesUtil.decrypt(dto.getData(), aesKey, iv); // 5. 把解密后的明文交给 Spring 正常的反序列化流程 CryptoContext.setAesKey(aesKey); byte[] plainBytes plainText.getBytes(StandardCharsets.UTF_8); return new MappingJacksonInputMessage( new ByteArrayInputStream(plainBytes), inputMessage.getHeaders()); } }真正机密的是第 5 步。beforeBodyRead返回的HttpInputMessage会被 Spring 当成请求体数据源继续做 JSON 反序列化和参数绑定。业务 Controller 里拿到的就是解密后的明文对象完全无感。这里有一个额外信息CryptoContext是 ThreadLocal 持有当前请求的 AES 密钥供响应加密时复用。请求结束后必须在拦截器或过滤器里清空否则线程池复用会串数据。3.3 响应自动加密ResponseBodyAdvice 实现响应方向逻辑类似。在beforeBodyWrite里拿到 Controller 返回的业务对象序列化成 JSON加密后封装成统一响应结构。注意返回值要是一个对象不要直接返回字符串否则会再次被序列化导致结构异常。RestControllerAdvice public class CryptoResponseAdvice implements ResponseBodyAdviceObject { Override public boolean supports(MethodParameter returnType, Class? extends HttpMessageConverter? converterType) { Class? clazz returnType.getContainingClass(); if (clazz.isAnnotationPresent(IgnoreCrypto.class) || returnType.hasMethodAnnotation(IgnoreCrypto.class)) { return false; } return clazz.isAnnotationPresent(EnableCrypto.class) || returnType.hasMethodAnnotation(EnableCrypto.class); } Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class? extends HttpMessageConverter? selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { byte[] aesKey CryptoContext.getAesKey(); byte[] iv AesUtil.generateIv(); boolean needTransportKey aesKey null; if (aesKey null) { aesKey AesUtil.generateKey(); } String plainJson JSON.toJSONString(body); String data AesUtil.encrypt(plainJson, aesKey, iv); CryptoResponseDTO result new CryptoResponseDTO(); result.setData(data); result.setIv(Base64.getEncoder().encodeToString(iv)); result.setTimestamp(System.currentTimeMillis() / 1000); result.setNonce(UUID.randomUUID().toString()); result.setSign(SignUtil.sign(data, result.getTimestamp(), result.getNonce(), aesKey)); if (needTransportKey) { PublicKey publicKey RsaKeyHolder.getPublicKey(); result.setEncryptKey(Base64.getEncoder().encodeToString( RsaUtil.encrypt(aesKey, publicKey))); } return result; } }这里的复用策略要注意请求里带了 AES 密钥的话响应直接复用客户端不必再做 RSA 解密性能最好。但如果你面对的是安全要求非常高的场景密钥用一次就轮换更稳响应里就重新生成一个 AES 密钥并 RSA 加密给客户端。我给出的代码两种都支持请求带密钥就复用没带就新建并传回。3.4 防重放与验签的代码实现防重放的核心NonceManager用 Redis 的 SETNX 原子操作实现Component public class NonceManager { Resource private StringRedisTemplate redisTemplate; public boolean checkAndSet(String nonce, long timestamp, long windowSeconds) { long now System.currentTimeMillis() / 1000; if (Math.abs(now - timestamp) windowSeconds) { return false; } Boolean ok redisTemplate.opsForValue().setIfAbsent( crypto:nonce: nonce, 1, Duration.ofSeconds(windowSeconds 60)); return Boolean.TRUE.equals(ok); } }缓存时间我故意比时间戳窗口多 60 秒。原因很简单两个请求如果时间戳都在窗口边缘紧挨着到达nonce 缓存先过期那攻击者即使超时重放也可能被放过。多留 60 秒能把这个漏洞堵上。验签工具类public class SignUtil { private static final String HMAC_ALGORITHM HmacSHA256; public static String sign(String data, long timestamp, String nonce, byte[] aesKey) { String content data timestamp nonce; Mac mac Mac.getInstance(HMAC_ALGORITHM); SecretKeySpec keySpec new SecretKeySpec(aesKey, HMAC_ALGORITHM); mac.init(keySpec); return Base64.getEncoder().encodeToString(mac.doFinal(content.getBytes(StandardCharsets.UTF_8))); } public static boolean verify(String data, long timestamp, String nonce, String sign, byte[] aesKey) { String expected sign(data, timestamp, nonce, aesKey); return MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), sign.getBytes(StandardCharsets.UTF_8)); } }这里必须注意两个细节。第一verify用MessageDigest.isEqual做常量时间比较不能直接用equals否则存在时间侧信道攻击风险。第二签名字符串的拼接顺序必须是data timestamp nonce客户端和服务端约定死任何一端改顺序都会导致验签失败。data作为密文本身就是 Base64 字符串拼接时不需要再加其他分隔符因为字段长度是固定的不会产生歧义。4. 配置、性能与上线审查4.1 密钥管理与轮换策略RSA 私钥是整个方案里最核心的资产。私钥一旦泄露所有加密保护全部失效。私钥文件绝对不能提交到代码仓库也不建议放在 Jar 包里。推荐的做法私钥从环境变量、配置中心或独立的密钥管理服务读取配置文件里只放公钥私钥路径通过CRYPTO_PRIVATE_KEY这类环境变量动态指定生产环境私钥权限收紧只有应用部署账号能读。密钥对生成可以直接用命令openssl genrsa -out private_key.pem 2048 openssl rsa -in private_key.pem -pubout -out public_key.pem轮换策略我建议采用双密钥并行。具体操作是提前生成一组新密钥把新公钥分发给客户端服务端先保留新私钥和旧私钥两把解密时先尝试新私钥失败再用旧私钥。等全部客户端都切换完成再下线旧私钥。这样能实现无缝切换不会出现密钥一换全部客户端同时报错的事故。4.2 性能实测与 AES 密钥复用这套方案在交付前做过一次压测我把体感数据分享出来。RSA 2048 私钥解密一次大约 1 到 3 毫秒服务端公钥加密在 0.5 到 1 毫秒AES-256 CBC 加解密一 KB 左右的数据耗时通常在 0.1 毫秒以内。也就是说单个请求增加的加解密耗时大约 2 到 4 毫秒占接口总耗时的比例很低。真正影响吞吐的点反而在 JSON 序列化和 Base64 编解码上所以没必要为了省时间在加密算法上做妥协。但如果你们的接口调用频率极高、单机 QPS 逼近瓶颈我建议做两个优化会话级 AES 复用登录成功或握手阶段生成一次 AES 密钥后续请求直接引用避免每个请求都做一次 RSA 解密。代价是密钥复用窗口拉长泄露后果更大适合中等安全等级场景避免重复加解密静态资源、内部探活接口、白名单接口都不要走加解密链路用IgnoreCrypto明确标记。4.3 灰度发布与开关设计全链路加密上线最大的风险是客户端和服务端升级不同步。我经历过一次服务端先上了强制加密结果旧版本客户端没更新线上直接全部解密失败虽然只有几分钟但足够让人血压拉满。所以代码里一定要预留开关。我一般会在配置里加一个crypto.enabled开关线上发布时先打开一半流量验证再逐步放量crypto: enabled: true private-key: ${CRYPTO_PRIVATE_KEY} public-key: ${CRYPTO_PUBLIC_KEY} timestamp-window-seconds: 300 nonce-cache-seconds: 360开发环境可以把开关关掉直接用 Postman 调明文接口调试不用每次拼密文报文。IgnoreCrypto注解则解决“同一个服务里部分接口不想加密”的问题比如对外提供公网 IP 白名单保护的内部接口。5. 常见问题与排查技巧实录这套方案在实际项目里跑过几个月踩了不少坑我把最典型的问题整理成速查表遇到类似场景可以直接对着排查。5.1 解密出来乱码、Base64 异常乱码问题九成是字符集不统一。加密前明文必须统一用 UTF-8 编码JSON 序列化时也要显式指定StandardCharsets.UTF_8。我之前遇到过一次前端用 JavaScript 的 TextEncoder 默认 UTF-8服务端用 String.getBytes() 没指定字符集Windows 环境下默认 GBK一解密就乱码。统一 UTF-8 后问题消失。Base64 异常则要关注 URL 安全变体。标准 Base64 里有、/、这些字符在 URL 传参时会被转义成%2B、%2F、%3D导致解码失败。这是我在接入 App 客户端时踩过的坑。解决办法是统一约定用 URL-safe Base64Java 里用Base64.getUrlEncoder()和Base64.getUrlDecoder()前端对应base64url编码。5.2 签名总是不通过签名不通过的常见原因有三个。第一签名拼接顺序不一致前端拼data timestamp nonce后端拼timestamp nonce data肯定对不上。第二HMAC 密钥传的是字符串而非字节数组前后端拿到同一把密钥却算出不同的签名一般是因为字符串转字节数组时字符集不一致。第三时间戳单位搞混前端传毫秒、后端按秒解析差了一千倍签名内容自然不同。我调试这样的问题习惯先把签名内容明文打出来对比前后端各自打印要签名的字符串一眼就能看出差异。千万别在验证阶段用“盲试”的方式效率太低了。5.3 调试工具直接调用报失败Postman、Swagger、curl 直接调加密接口大概率报“解密失败”。这不是程序 bug是调试工具不会自动走加密流程。正确做法是开发环境把crypto.enabled关掉或者给文档页面接口加IgnoreCrypto。如果一定要联调加密报文我建议写一个简单的在线加密工具页输入明文自动生成加密报文再粘贴到 Postman 里比纯手拼效率高很多。5.4 拦截器污染 Body、日志泄漏如果你用 Filter 做加解密切记不能用 Filter 直接读getInputStream()还不做包装否则 Controller 参数绑定拿到的会是空对象。Spring 里要包装ContentCachingRequestWrapper让流被多次读取。不过我建议能不用 Filter 就不用RequestBodyAdvice的方案更干净。日志这个坑更隐蔽。解密后随手打一条 log把 URL 和参数都打出来结果日志平台一搜关键词加密形同虚设。如果业务上必须打日志也要只打解密后的非敏感字段或者打哈希值、掩码过的脱敏值。5.5 常见问题速查表现象原因解决办法解密后中文乱码字符集不统一前后端统一 UTF-8Base64 解码抛异常URL 转义导致/被替换统一使用 URL-safe Base64签名验证失败拼接顺序、字符集、时间戳单位不一致前端打印拼签字符串对齐请求被误判重放时钟偏差超过窗口窗口放宽到 5 分钟同时 NTP 同步服务端时间Postman/Swagger 直接调失败调试工具未走加密链路开发环境关闭开关或使用IgnoreCrypto拦截器读后参数为空Filter 未包装请求流使用 ContentCachingRequestWrapper 或基于 Advice 实现日志中出现明文解密后打日志只记录脱敏字段严禁完整明文日志这套全链路加解密方案里代码本身只是其中一个环节真正决定成败的是细节密钥怎么存、开关怎么切、日志怎么打、签名怎么拼。这些坑我都替你踩过一遍了照着上面的方案落地至少能省下两个晚上的排查时间。如果你正在设计接口安全方案或者想给现有系统加一层保护希望这份整理能帮到你。
阅读完成 · 觉得有帮助?
咨询建站