news 2026/9/24 18:18:30

API接口敏感数据加解密实战:AES+RSA混合加密与Spring Boot透明接入

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
API接口敏感数据加解密实战:AES+RSA混合加密与Spring Boot透明接入

前两周给一家做医疗信息化的团队做技术评审,对方安全负责人提了个很现实的需求:身份证号、手机号、银行卡这些字段在接口传输里全是明文,虽然开了HTTPS,但等保评测和客户审计都盯着这一点,要求“不能直接看到明文”。这种场景在金融、医疗、政务、企业服务里越来越常见,API接口的敏感数据加解密,已经不是“要不要做”的问题,而是“怎么做得优雅”的问题。

这里的“优雅”有两个层面:一是技术方案本身要经得起推敲,性能损耗可控、密钥管理安全、兼容性处理好;二是对业务代码的侵入要最小化,别让每个Controller都手写一遍加解密逻辑。这篇文章我把整套方案的演进过程、选型思路、落地代码和线上踩坑完整拆开讲,适合后端开发、架构师以及对接口安全有要求的团队参考。

1. 一个被审计逼出来的思考:HTTPS 为什么兜不住敏感数据

1.1 反直觉的真相:HTTPS 只保护传输管道,不保护接口本身

很多人觉得上了HTTPS就可以高枕无忧,这是典型的“传输层安全”思维惯性。HTTPS解决的是客户端和服务端之间的链路窃听与中间人问题,数据到了服务端之后,在网关层、业务层、日志系统里流转时,已经没有任何保护。换句话说,HTTPS保护的是“路”,不是“货”。

举几个实际场景你就明白了:

  • 第三方系统通过接口拉取用户手机号,对方服务端把请求日志打到ELK里,URL参数或者Body里明晃晃的手机号全量落盘,一旦ELK泄露,整库数据裸奔。
  • 你们自己的网关层通常会打印调试日志,线上排查问题时顺手把请求体打出来,里面有身份证号。运维同事截图发到群里,数据就这么“合法”地出去了。
  • 接口返回给前端的敏感字段是明文,前端的内存、浏览器的开发者工具、第三方统计脚本,都有可能接触到这些数据。

安全审计、等保合规、客户合同里的数据安全条款,都会要求敏感数据在接口传输层具备加密能力。这个“传输层”指的不光是SSL/TLS,还包括HTTP报文本身的承载内容。所以在应用层再做一层加解密,是合理的互补,而不是重复建设。

1.2 接口加解密要解决的四个核心问题

在动手设计之前,先把需求拆明白。一套完整的接口敏感数据加解密方案,至少要覆盖四个问题:

机密性。报文体里的敏感字段不能是明文,抓包、日志、中间链路都读不到真实数据。这是最基本的要求。

完整性。数据在传输过程中不能被篡改,哪怕加密了,也要防止攻击者把密文替换成别的密文,或者截断重放。所以光有加密不够,还得有签名或MAC机制。

防重放。攻击者把之前截获的合法密文请求原样重放一遍,服务端能不能识别并拒绝?这需要时间戳、随机数或流水号加上服务端缓存来做。

对业务透明。业务代码里不应该出现“解密”“加密”这样的调用,否则几十个接口每个人写一遍,一定会有人写错、漏写。优雅的方案是把这个能力下沉到框架层或中间件层。

这四个问题不是并列关系,而是层层递进。很多人做接口加密只做了第一点,把字段用AES一加密就上线了,这是典型的“只做了半套”。完整方案里的签名和防重放,才是区分专业和业余的分水岭。下面我先讲方案选型,再讲怎么把这些能力透明地嵌入到现有系统里。

2. 选型博弈:对称、非对称与混合加密,为什么最终选了混合方案

2.1 三种路线的对比与适用边界

接口数据加解密,底层逃不开对称加密和非对称加密这两个家族。绕开算法细节,直接说结论。

对称加密(AES)的特点是快,加解密吞吐量能达到Gbps级别,对业务接口的耗时影响微乎其微。但痛点在于密钥怎么安全地传给对方。如果直接用AES做接口加密,你得把同一个密钥写在客户端和服务端的配置里,一旦密钥泄露,全部历史报文都能被解开,而且无法追溯是哪个环节泄露的。

非对称加密(RSA)的特点是安全性模型好,公钥加密、私钥解密,公钥可以公开分发。但慢,尤其是RSA私钥解密2048位密文的性能比AES慢几个数量级。用RSA加密大数据体(超过密钥容量)还有长度限制,需要分段处理,复杂度直接起飞。

混合加密的思路是扬长避短:用RSA来协商一个临时的AES密钥,再用这个AES密钥去加密实际的数据。每次请求都可以生成一个新的AES密钥(术语叫“一话一密”),用对方的公钥加密后传过去。对端拿到后用私钥解开得到AES密钥,再解数据。这样既有AES的高性能,又有RSA的安全分发能力。

实际项目中,我基本都会推荐混合加密。可能有人会说,那直接用国密SM2/SM4不更好?国密当然可以,合规要求必须用的时候直接用,方案框架完全一样,把算法参数换掉就行。下面的实现我还是以AES+RSA这个最通用、最容易表述的组合来讲。

2.2 混合加密的完整链路:一话一密 + 密钥协商

把一次完整的加密请求链路画出来,就是下面这个流程:

  1. 客户端生成一个随机的AES对称密钥(比如16字节),这个密钥只用于本次请求。
  2. 客户端用服务端的RSA公钥加密这个AES密钥,得到密文Key。
  3. 客户端用AES密钥加密请求体的业务数据,得到密文Data。
  4. 客户端把密文Key + 密文Data + 随机IV + 签名 + 时间戳 + 随机数封装成统一报文,发给服务端。
  5. 服务端先用RSA私钥解密密文Key,得到AES密钥。
  6. 服务端再用AES密钥解密Data,还原业务数据。
  7. 服务端业务逻辑处理完,用同一个AES密钥加密响应数据,返回给客户端。
  8. 客户端用自己生成的AES密钥解密响应。

这个流程的关键点在于:AES密钥是每次请求动态生成的临时密钥。服务端不需要保存这个密钥,客户端也不需要预配置这个密钥,它只是在这一问一答的瞬间存在。这样就避免了传统对称加密方案里“密钥静态存储”的风险。

RSA在整个链路里只做一件事:保护AES密钥的传输。AES只做一件事:加密真实业务数据。各司其职,这个设计是整个方案的核心骨架。

3. Spring Boot 落地:拦截器层完成透明加解密,业务代码零侵入

3.1 加密数据包的协议格式设计

动手写代码之前,先定义好报文格式。这个格式一旦定下来,客户端和服务端都要跟着走,所以要考虑周全:

{ "encryptKey": "Base64编码的RSA加密后AES密钥", "iv": "Base64编码的AES初始向量", "data": "Base64编码的AES密文", "sign": "HMAC-SHA256签名值", "timestamp": "毫秒级时间戳", "nonce": "随机字符串,防止重放" }

几个字段各司其职:

  • encryptKeyiv负责让服务端恢复出AES密钥和初始向量。IV每次随机生成,确保即使两次请求的明文数据相同,密文也不同。这是一开始就要定的规范,不然后面想加都没法平滑加。
  • data是真正的业务密文。我建议统一做成Base64编码的字符串,避免二进制流在JSON里被转义、编码折腾。
  • sign是对encryptKey + iv + data + timestamp + nonce做的HMAC签名,密钥单独走一套签发体系,用来保证报文整体不可篡改。
  • timestampnonce组合起来做防重放,后面的章节单独展开。

响应报文的格式和请求对称,客户端用自己生成的AES密钥直接解data字段即可。响应里的encryptKey可以省略,因为客户端本身就知道AES密钥是什么,服务端返回响应时也不需要重新协商密钥。

3.2 服务端过滤器实现:请求解密与响应加密

服务端最优雅的实现方式是用Filter,而不是AOP,也不是在每个Controller里手动调用。原因很简单:Filter在Servlet容器的最早阶段就能拦截到原始请求,并且能包装请求体、在返回前包装响应体。业务代码完全无感知,甚至连Controller的入参都不会意识到自己经历过解密。

先定义一个注解,用来标记哪些接口需要走加解密:

@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiCrypto { // 标记该接口的请求体和响应体需要进行加解密 }

然后是基于注解判断的加密过滤器。判断逻辑用到RequestMappingHandlerMapping,在过滤器中通过getHandler(request)提前拿到要执行的HandlerMethod,检查方法或者类上有没有@ApiCrypto注解:

@Component public class ApiCryptoFilter extends OncePerRequestFilter { @Autowired private RequestMappingHandlerMapping handlerMapping; @Autowired private CryptoService cryptoService; // 核心加解密服务 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 只处理带 @ApiCrypto 注解的接口 ApiCrypto crypto = resolveCryptoAnnotation(request); if (crypto == null) { chain.doFilter(request, response); return; } // 1. 解密请求体 CryptoRequestWrapper requestWrapper = new CryptoRequestWrapper(request); String plainText = cryptoService.decryptRequest(requestWrapper.getBody()); requestWrapper.setBody(plainText); // 2. 包装响应,以便在返回前统一加密 ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); try { chain.doFilter(requestWrapper, responseWrapper); } finally { // 3. 把业务写入的响应体取出来,加密后重新写回 byte[] responseBody = responseWrapper.getContentAsByteArray(); String encryptedBody = cryptoService.encryptResponse(responseBody); responseWrapper.resetBuffer(); responseWrapper.getOutputStream().write(encryptedBody.getBytes(StandardCharsets.UTF_8)); responseWrapper.copyBodyToResponse(); } } private ApiCrypto resolveCryptoAnnotation(HttpServletRequest request) { try { HandlerExecutionChain chain = handlerMapping.getHandler(request); if (chain == null) { return null; } Object handler = chain.getHandler(); if (handler instanceof HandlerMethod handlerMethod) { ApiCrypto methodCrypto = handlerMethod.getMethodAnnotation(ApiCrypto.class); if (methodCrypto != null) { return methodCrypto; } ApiCrypto classCrypto = handlerMethod.getBeanType().getAnnotation(ApiCrypto.class); return classCrypto; } } catch (Exception e) { // 这里不建议抛异常,保持过滤器的容错性 log.warn("解析加密注解失败", e); } return null; } }

这里有个细节很多人会忽略:RequestMappingHandlerMapping.getHandler内部可能会执行跨域、拦截器等前置逻辑,我在实际项目里加了全局异常兜底,避免解析失败时直接让请求404。

CryptoRequestWrapper的作用是把请求流缓存下来,因为Filter里读一次请求体之后,Controller就读不到了。实现方式是用HttpServletRequestWrapper重写getInputStreamgetReader

public class CryptoRequestWrapper extends HttpServletRequestWrapper { private byte[] body; public CryptoRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.body = IOUtils.toByteArray(request.getInputStream()); } public String getBody() { return new String(body, StandardCharsets.UTF_8); } public void setBody(String content) { this.body = content.getBytes(StandardCharsets.UTF_8); } @Override public ServletInputStream getInputStream() { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() { return byteArrayInputStream.read(); } @Override public boolean isFinished() { return byteArrayInputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) {} }; } @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } }

注意setBody这个方法是自定义的,Spring MVC里注解@RequestBody解析时走的还是getInputStream(),所以从底层把字节流替换掉是成立的。整个 Filter 的加解密过程,对 Controller 层完全不可见。

3.3 核心加解密服务:AES+RSA 混合实现的完整代码

CryptoService是整套方案的心脏,包含解密请求和加密响应两个方法。先看解密:

@Service public class CryptoService { @Value("${api.crypto.rsa.private-key}") private String rsaPrivateKey; @Value("${api.crypto.sign.secret}") private String signSecret; private static final int AES_KEY_SIZE = 128; private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding"; private static final String AES_ALGORITHM = "AES/GCM/NoPadding"; public String decryptRequest(String encryptedBody) { // 1. 解析统一报文 JSONObject payload = JSON.parseObject(encryptedBody); String encryptKey = payload.getString("encryptKey"); String iv = payload.getString("iv"); String data = payload.getString("data"); String sign = payload.getString("sign"); long timestamp = payload.getLongValue("timestamp"); String nonce = payload.getString("nonce"); // 2. 验签(逻辑见第4章) verifySign(encryptKey, iv, data, timestamp, nonce, sign); // 3. RSA私钥解开AES密钥 byte[] aesKeyBytes = rsaDecrypt(Base64.getDecoder().decode(encryptKey)); SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); // 4. 用AES密钥解密业务数据 byte[] plainBytes = aesGcmDecrypt(Base64.getDecoder().decode(data), Base64.getDecoder().decode(iv), aesKey); return new String(plainBytes, StandardCharsets.UTF_8); } private byte[] rsaDecrypt(byte[] cipherBytes) { try { PrivateKey privateKey = KeyFactory.getInstance("RSA") .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(rsaPrivateKey))); Cipher cipher = Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(cipherBytes); } catch (Exception e) { throw new RuntimeException("RSA解密失败", e); } } private byte[] aesGcmDecrypt(byte[] cipherData, byte[] iv, SecretKeySpec key) { try { Cipher cipher = Cipher.getInstance(AES_ALGORITHM); GCMParameterSpec spec = new GCMParameterSpec(128, iv); cipher.init(Cipher.DECRYPT_MODE, key, spec); return cipher.doFinal(cipherData); } catch (Exception e) { throw new RuntimeException("AES解密密失败", e); } } public String encryptResponse(byte[] responseBody) { // 每个请求的AES密钥从哪来? // 这里用ThreadLocal,在decryptRequest时把当前请求的AES密钥暂存 SecretKeySpec aesKey = CurrentCryptoContext.getAesKey(); byte[] iv = CurrentCryptoContext.getIv(); byte[] cipher = aesGcmEncrypt(responseBody, iv, aesKey); JSONObject result = new JSONObject(); result.put("data", Base64.getEncoder().encodeToString(cipher)); result.put("iv", Base64.getEncoder().encodeToString(iv)); return result.toJSONString(); } }

响应加密这里有一个容易绕晕的点:AES密钥是客户端生成的,服务端怎么拿它加密响应?所以服务端必须在解密请求时把这个AES密钥用ThreadLocal缓存到当前线程上下文中,等Controller处理完、过滤器准备加密响应时,再从这个上下文里取出来用。

public class CurrentCryptoContext { private static final ThreadLocal<SecretKeySpec> AES_KEY = new ThreadLocal<>(); private static final ThreadLocal<byte[]> IV = new ThreadLocal<>(); public static void set(SecretKeySpec key, byte[] iv) { ... } public static SecretKeySpec getAesKey() { return AES_KEY.get(); } public static byte[] getIv() { return IV.get(); } public static void clear() { AES_KEY.remove(); IV.remove(); } }

使用ThreadLocal时要特别注意清空,防止线程池复用导致串数据。我在finally块里调用CurrentCryptoContext.clear(),这是线上容易漏掉但又必须做的一步。

AES的GCM模式自带认证能力,密文被篡改后解密时会直接抛异常,这给了我们第一层完整性保护。但GCM只保护data字段本身,不保护encryptKeytimestampnonce这些元数据。所以还需要单独做签名来覆盖整个报文,这就是第4章的内容。

4. 签名、时间戳与流水号:把防篡改和防重放补完整

4.1 为什么有加密还不够:中间人改请求怎么办

加密能防“看”,但防不了“改”。攻击者虽然解不开你的AES密文,但他可以把你整个密文报文原封不动地截下来,然后一遍一遍地重放。还可以把密文报文里的加密密钥替换成一个他自己生成的密钥,发给服务端,服务端用私钥解不出来,直接报错,这算是DoS。更隐蔽的是,如果接口设计得不好,重放一次“转账请求”就是一次真实的转账操作。

所以加密必须搭配三个机制:签名字段保证完整性和来源可信;时间戳保证请求的时效性;nonce+缓存保证一个请求只能被执行一次。这三者配合,才能堵住上面的漏洞。

4.2 HMAC 签名与验签的落地方案

签名的算法选择上,HMAC-SHA256是标准做法。签名用的密钥是单独从服务端和客户端约定的(通过配置中心下发),不能直接用RSA私钥或者AES密钥。理由很简单:如果混用密钥,一个环节泄露就可能连累其他环节。

签名的计算逻辑在客户端和服务端要保持完全一致,我把伪代码写出来:

public String computeSign(String encryptKey, String iv, String data, long timestamp, String nonce, String secret) { String content = encryptKey + "&" + iv + "&" + data + "&" + timestamp + "&" + nonce; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] hash = mac.doFinal(content.getBytes(StandardCharsets.UTF_8)); return HexUtil.encodeHexStr(hash); }

服务端验签时先按同样的规则计算出本地签名,再与报文里的sign做恒定时间比较(防止时序攻击):

private void verifySign(String encryptKey, String iv, String data, long timestamp, String nonce, String sign) { // 1. 时间窗校验:允许5分钟时钟偏差 long now = System.currentTimeMillis(); if (Math.abs(now - timestamp) > 5 * 60 * 1000L) { throw new InvalidSignatureException("请求时间戳已过期"); } // 2. 重放校验(见4.3) // 3. 签名比对 String localSign = computeSign(encryptKey, iv, data, timestamp, nonce, signSecret); if (!MessageDigest.isEqual(localSign.getBytes(StandardCharsets.UTF_8), sign.getBytes(StandardCharsets.UTF_8))) { throw new InvalidSignatureException("签名校验失败"); } }

有了签名,攻击者想改任何一个字段都会导致签名对不上,服务端直接拒绝。同时签名密钥是独立的,即使RSA私钥泄露,签名体系还可以单独轮换,不至于整体推翻。

4.3 重放攻击的拦截:timestamp + nonce + Redis

时间戳能挡住大部分“延迟重放”,但挡不住“5分钟内的快速重放”。所以要配合nonce:服务端把每个处理过的timestamp + nonce组合存到Redis里,设置过期时间5分钟。同一个nonce在有效期内第二次出现,直接拒绝。

private void checkReplay(long timestamp, String nonce) { String redisKey = "api:crypto:replay:" + timestamp + ":" + nonce; Boolean firstSeen = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", Duration.ofMinutes(5)); if (Boolean.FALSE.equals(firstSeen)) { throw new InvalidSignatureException("重复请求,已拒绝"); } }

用Redis的SETNX指令做这个事是天然原子的,不需要加锁。高并发场景下需要注意:如果同一个nonce真的并发到达,Redis能够保证只有一个请求会拿到true,其余全部拒绝。

对于非重放校验的幂等性,我补充一下:重放拦截和业务幂等不是一回事。重放拦截要求每次请求的nonce都不相同,而业务幂等要求同一笔业务请求即使发送多次也只会生效一次。所以在转账、下单这类接口里,服务端还应该用自己的业务流水号和幂等表再兜一层,两套机制是互补关系。

5. 实测数据与踩坑记录:性能损耗、字符集和 Base64 陷阱

5.1 压测结果:加解密对整体耗时的影响

方案上线前要测一下性能损耗,不然上线后被性能问题搞到回滚就尴尬了。我用的是8核16G的虚拟机,Spring Boot应用,压测工具用wrk模拟100并发跑30秒。

结果如下:

场景平均耗时(ms)TP99(ms)QPS
明文直通(基线)28423400
全报文加密(AES+RSA)35512900

加密后平均耗时增加约7ms,QPS下降约15%。这7ms里,大头是RSA私钥解密AES密钥那一次操作(大约2-4ms),AES加解密数据本身对几百字节的报文来说几乎可以忽略。如果换成SM2/SM4国密算法,RSA换成SM2之后耗时会更明显一点,但一般也不会超过20ms。

这个性能损耗对绝大多数业务接口来说是可以接受的。如果你们有超高频的接口(每秒几万次),建议只对包含真正敏感字段的接口开启加密,不要全局一刀切。另外可以上RSA缓存优化:如果客户端能支持会话级的密钥协商(一次协商多次使用),能进一步减少RSA调用,但复杂度会高很多,一般业务量级没必要。

5.2 踩过的三个坑:字符集、RSA长度限制、报文体积膨胀

先说字符集。RSA解密出来的字节数组转成AES密钥时,直接用就够了。但encryptKey在传输过程中经过JSON序列化、Base64转码,两端之间容易出UTF-8和ASCII的编码错位问题。我的经验是全程统一UTF-8,Base64之后不要再用URLEncoder二次编码,需要放URL里的时候就换成Base64URL安全版本(把+和/替换成-和_)即可。

然后是RSA长度限制。RSA用2048位密钥、PKCS1Padding时,最多只能加密245字节。AES的key加IV也不超过32字节,所以一般不会撞到上限。但有一种情况会:如果你设计成“用RSA加密一个包含大量额外信息的JSON串”,比如把签名密钥、过期时间、用户ID都放进RSA加密的payload里,很容易超过245字节,然后报Data must not be longer than 245 bytes。所以RSA加密的内容越短越好,职责单一。

最后是报文体积膨胀。Base64本身增加33%体积,AES密文又有填充,加密后整个请求体可能是明文的1.4-1.5倍。如果接口本来就有几MB的上传报文,膨胀会更严重。所以大文件、大报文不要走应用层加解密,应该走HTTPS + 对象存储的服务端加密,或者分块加密,不要混在一个方案里。

5.3 日志脱敏:忘了这步等于白做

接口加解密上线后,最容易出现的一个“白做”环节是日志。请求到了服务端,Filter解完密之后,Controller打印日志、业务打印日志,如果把明文写进日志,那加密就形同虚设了。而且这比HTTPS的问题更严重——日志数据是长期保存的,泄露后无法撤回。

我的习惯是两件事同时做:

第一件,在Filter层做一个脱敏的日志切面。解密后的JSON体如果最终要打印,统一走一个DesensitizedJsonUtils,把phoneidCardbankCardname这类字段的值只在日志里保留前后两位和掩码符号。

第二件,在日志框架层面配置一个全局过滤器。Logback里实现了MessageConverter可以拦截所有输出内容,把常见敏感字段正则匹配后打上掩码。这样即使有人不小心在代码里log.info("用户身份证:{}", idCard),打出来的也还是脱敏后的内容。

这两道防线不冲突,都上了才算闭环。我在评审时遇到很多团队,加密做了、签名做了、防重放做了,最后死于一行日志,这是非常可惜的。

6. 密钥管理、灰度切换与后续演进

6.1 密钥存储:一两行配置承载不了高风险

方案里的私钥和签名密钥如果直接写在application.yml里,等于把保险柜钥匙挂在保险柜上。生产环境的私钥应该放到专门的密钥管理系统(KMS)里,代码运行时通过网络接口获取私钥,而不是在配置中心明文分发。如果没有KMS,至少要保证以下几点:

  • 私钥不出内网,不能进Git仓库,不能出现在日志中。
  • 私钥文件权限收紧到服务运行用户只读。
  • 签名密钥和RSA私钥分开保存、分开轮换。
  • 定期轮换密钥,并保证轮换时新旧密钥有一段重叠生效期,让客户端平滑切换。

密钥轮换的一个实际做法是给密钥加版本号,报文里带上密钥版本标识,服务端根据版本号选择对应的解密密钥。我见过一个客户就是没做版本号,轮换密钥当天所有存量客户端全部调不通,最后只能紧急回滚。这个坑不踩一次很难意识到版本号的重要性。

6.2 双版本灰度与客户端兼容

接口安全升级最怕的是“客户端没跟着升级,服务端先切了”。所以上线策略建议走双版本灰度:

服务端保留明文接口和密文接口两个入口,通过配置开关控制哪些客户端、哪些接口走加密。客户端SDK里内置版本号,请求时带上crypto-version头,服务端根据版本分发到不同处理链路。灰度顺序:先让内部系统切到加密链路,再灰度核心合作伙伴,最后强制全部切换。

如果你们是纯自研的B/S系统,前后端都是自己的,灰度会更简单。前端SDK统一封装好加解密逻辑后,发版顺序还是建议“先前端后后端”,确保所有请求到了服务端都是能被解密的状态。

还有一个细节:加解密失败时的错误响应不要明文返回具体异常栈。可以用统一的错误码,比如40001加密参数缺失、40002验签失败、40003解密异常,避免攻击者通过异常信息探测算法和密钥结构。

6.3 后续可能的扩展方向

这套方案做完以后,还能往几个方向延伸。

一是把加解密能力下沉到API网关层。如果公司有统一的网关,可以在网关做一次统一的解密,内部服务之间继续走明文,这样业务服务的改造量更小。但要注意:网关解密后内部明文暴露面变大,适用于内部网络可信度较高的场景。

二是在响应敏感字段上按需加密。有些接口大部分字段可以明文,只有某个字段需要加密。这种情况下可以在报文里定义一个encryptFields数组,服务端只对指定字段加密。相比全报文加密,按字段加密的兼容性和性能都更好,但方案复杂度会上升。我一般建议先用全报文加密,简单直接,等业务量大了再考虑按字段优化。

三是适配国密算法。如果你们的客户或监管有国密要求,把AES换成SM4、RSA换成SM2,整体框架不变,只是替换具体实现和密钥生成方式。这个我在项目里做过一次切换,改动量可控,关键是把算法实现封装在CryptoProvider单一接口后面,不要散落在业务代码里。

最后分享一个我个人的经验:这种安全方案,一定要专门拿出来一部分时间做“攻击推演”,而不是只写代码。拿你自己写的过滤器,试着用Burp Suite重放一个请求,试着改一下签名里的任意一个字符,试试超过时间窗口的请求能不能被拒。自己先攻击一轮,比上线后被安全团队打回来要体面得多。我每次上线安全相关功能都会做一遍这个流程,抓出来的问题比测试用例多得多,也少挨了很多批评。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 18:18:01

JavaEE图书管理系统实战:Spring Boot+MyBatis选型、并发借阅与避坑指南

简介&#xff1a;这是一套面向JavaEE初学者与课程设计者的图书管理系统完整源码&#xff0c;基于MVC三层架构实现&#xff0c;适合用于毕业设计、课程作业或企业级开发入门练手。压缩包共93个文件&#xff0c;约6.62MB&#xff0c;以java源文件、jsp页面、xml配置、class字节码…

作者头像 李华
网站建设 2026/9/24 18:16:54

2026年头戴式耳机怎么选?10款热门机型横评推荐

又是一年盘点时间。前两天在后台看到一条留言&#xff0c;问我“2026年了&#xff0c;头戴式耳机到底还有没有买的必要”&#xff0c;说实话这个问题本身就很能代表一部分人的心态&#xff1a;手机厂商都在卷TWS&#xff0c;头戴式耳机这个品类看着好像没那么“便携”&#xff…

作者头像 李华
网站建设 2026/9/24 18:16:25

基于YOLOv8的智慧工厂危险区域闯入识别系统:从训练到部署

简介&#xff1a;这份资源面向计算机、人工智能、自动化等专业的在校学生与教师&#xff0c;提供一套可直接运行的智慧工厂危险区域闯入识别方案&#xff0c;适合作为毕业设计、课程设计或大作业的完整参考。项目以YOLOv8目标检测为核心&#xff0c;配套可视化界面&#xff0c;…

作者头像 李华
网站建设 2026/9/24 18:16:20

远程3D渲染全攻略:GPU算力云、远程桌面与硬件选型实战指南

远程3D渲染这个词&#xff0c;这几年被提得越来越频繁。2026年再看这件事&#xff0c;我觉得核心就一句话&#xff1a;把算力留在机房&#xff0c;人回家。过去做三维设计的人&#xff0c;基本被一台高性能工作站绑在工位上&#xff0c;机器在哪&#xff0c;人就得在哪。现在不…

作者头像 李华
网站建设 2026/9/24 18:16:00

Windows 开机自动录屏 8 种方案详解:从任务计划程序到 FFmpeg 实战

Windows 上实现开机自动录屏&#xff0c;听上去是个很小的需求&#xff0c;真做起来却有一堆讲究。我最近因为要给一台无人值守的机器做操作记录&#xff0c;把 Win10 22H2 和 Win11 23H2 上能用的开机自动录屏方案挨个测了一遍&#xff0c;发现最大的坑不是"找不到方法&q…

作者头像 李华