ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

接口安全加固:AES-GCM+RSA混合加密与Spring Boot落地实践

接口安全加固:AES-GCM+RSA混合加密与Spring Boot落地实践

干接口安全的同学,应该都有过这种体会:Restful接口的入参和返回参数直接明文在网络上传输,光着身子跑,看着心里就不踏实。只要对方抓到包,请求结构、字段含义、业务逻辑几乎一览无余;如果后端日志再把完整报文打出来,那数据泄露就只是时间问题。这篇文章是我在一套服务端项目里落地“入参与出参安全加解密”的完整复盘,包含算法选型、密钥协商、统一拦截实现,以及后来排查乱码、超时、签名校验失败时的那些坑。适合正在给接口做安全加固、被要求“把敏感接口的数据都加密一下”的同学参考,也适合想弄清应用层加密到底怎么落地的人。

1. 为什么要折腾入参和返回参数的加解密

1.1 明文接口的三类现实风险

很多人觉得“我有HTTPS就够了”,但实际项目中明文接口的风险点比想象中多。

第一类是抓包暴露。HTTPS能防住链路层的窃听,但防不住“终端侧被信任的攻击者”。比如用户在手机上抓自己App的流量,或者一个第三方SDK把接口请求转发出来,又或者客户端被动态调试、注入后主动把请求打给代理,这些场景下HTTPS证书链路是完整的,但业务数据照样裸露。入参里面的手机号、身份证、银行卡,返回参数里的余额、订单明细、发票信息,全都可以被看得清清楚楚。

第二类是日志泄露。很多系统上线时开了全量报文日志,请求体和响应体原样打印。一旦日志系统权限失控、日志文件被拖走,敏感数据的泄露面比抓包还大。而且日志通常是长期留存,追溯和脱敏成本极高。

第三类是篡改与重放。明文接口的参数容易被恶意修改后重新提交。比如下单接口的金额字段,如果只做了前端校验,后端拿到的数据被改过也校验不出来;再比如抢购类的接口,别人录下你的正常请求,反复重放也能达到攻击效果。加解密并不是只为了“保密”,签名校验本身就是加密体系里不可拆的一部分。

1.2 什么场景真正需要应用层加解密

不是所有接口都需要加密,这个要清醒。公开的资讯类页面、商品列表、静态配置接口,加密只会带来无谓的CPU开销和排查难度。真正需要做的是这一批:

  • 涉及用户隐私的:手机号、邮箱、身份证、地址、设备信息
  • 涉及资金操作的:下单、支付、退款、转账、余额查询
  • 涉及账号体系的:登录、注册、改密、Token刷新
  • toB对接中的敏感业务字段:采购价、结算数据、合同相关的结构化数据
  • 合规要求明确要求数据加密存储和传输的场景

我之前在的那个项目,上线前被安全评审整整改了两轮,第一轮就是“敏感字段明文传输”,第二轮是“日志中存在敏感数据”。最后方案落地时,直接定了两层:HTTPS负责传输链路安全,应用层加解密负责业务数据本体安全,两件事互不替代。

1.3 为什么不能只依赖HTTPS

把话说透一点。HTTPS解决的是“管道安全”,它防的是管道上的人,但你防不住管道两端的“内部人”和“被攻陷的端点”。

我见过一个典型案例:客户端和服务端用HTTPS通信,安全评审时一切正常,结果某次上线后日志系统接入了第三方检索平台,开发人员为了排查问题把完整报文打进了日志,日志平台权限没收紧,一个离职员工的运维账号还能查到半年前的请求记录。这种情况下HTTPS是全程都在,但敏感数据还是泄露了。

应用层加解密的意义是:就算数据被从日志、数据库备份、客户端缓存、抓包工具里拿到,也是一堆密文和签名,没有密钥根本无法还原。这就是常说的纵深防御,每一层都有自己的职责。

2. 方案选型:先定算法,再谈实现

2.1 加密算法怎么选

算法选型是整个方案的基石,选错了后面全是坑。我直接说我用下来的结论。

对称加密选AES-GCM,而不是大家更熟悉的AES-CBC。GCM是“认证加密”模式,加密的同时产出认证标签,能同时保证机密性和完整性。CBC模式只加密不认证,攻击者可以翻转密文里的某些位来篡改明文,自己还得额外做签名或MAC,链路复杂还容易做错。AES-GCM在OpenSSL里有硬件加速,性能上并不比CBC差,Java侧内置支持,不需要引任何额外依赖。

密钥协商用RSA-OAEP,或者直接上国密SM2也行。RSA用的场景最广,服务端生成密钥对,公钥给客户端,私钥留在服务端。客户端每次请求生成一个随机的AES会话密钥,用RSA公钥加密后随请求发给服务端,服务端用私钥解出AES密钥,再做真正的报文加解密。这就是最经典的“混合加密”。

签名用HMAC-SHA256,别用MD5加盐这种自己拼的方案。HMAC是标准算法,实现简单、碰撞难度高,能同时防止请求被篡改。我这边在RSA以外再补一层HMAC,因为RSA只保护了会话密钥本身,而具体报文是否被改动,需要报文级签名来确认。

国密算法(SM2/SM4)我用过一段时间,主要是银行类项目要求国产化合规。技术上完全可行,但实现上要依赖第三方库,比如BouncyCastle,在部分发行版上要额外加JCE策略。如果不是硬性合规要求,通用项目先用AES-GCM+RSA更省心。

2.2 混合加密的完整流程

整个请求链路我拆成了五步,每一步都有明确职责:

  1. 客户端调用登录或密钥接口,获取服务端的RSA公钥(含密钥版本号)。
  2. 客户端生成一个临时的AES-256会话密钥,并生成随机的IV(Initialization Vector,初始向量)和Nonce。
  3. 业务报文用AES-GCM加密,得到密文和认证标签;会话密钥用RSA公钥加密,得到密钥密文。
  4. 客户端把“密钥密文 + 报文明文密文 + 时间戳 + Nonce + 签名”组装成统一请求结构,发送给服务端。
  5. 服务端用RSA私钥解出会话密钥,再用会话密钥解密报文,校验签名和时间戳,然后执行业务逻辑。

响应方向就是反过来。服务端也可以用同样的方式生成一个临时的响应密钥,或者复用客户端的会话密钥来加密响应报文。我在实际项目中是复用了客户端密钥,减少一次RSA解密,提高响应速度,但前提是整体链路的密钥生命周期管理做干净了。

关于AES的IV,必须每次随机生成,绝不能固定。固定IV会让相同明文产出相同密文,攻击者能通过密文比对推断业务状态变化,等同于把加密废了一半。

2.3 请求和响应的报文格式设计

统一格式方便前端排障和服务端解析。我用的结构是这样:

{ "version": 1, "timestamp": 1699999999000, "nonce": "6f8a9c21d4e7412ba91d", "encKey": "base64(RSA加密后的AES会话密钥)", "sign": "base64(HMAC-SHA256签名)", "payload": "base64(AES-GCM加密后的业务报文)" }

version是密钥版本号,服务端切换密钥时旧版本请求还能正常解密,不用强制客户端热更新。timestamp用来做重放防护,我一般允许前后5分钟的时间偏移,超过直接拒绝。nonce是一次性随机串,和时间戳一起参与签名,避免相同报文反复重放。payload是加密后的业务数据,整体是Base64编码的字符串。sign是用双方约定好的密钥对“timestamp + nonce + payload”做的签名,防止中间人改掉任何一个字段。

响应格式我保持一致,客户端解密逻辑可以复用。唯一区别是响应里可以不带encKey,直接用请求里协商好的会话密钥即可。

为什么不把整个body直接做成一个密文字符串?我也试过,比如{"data":"一串密文"},简单是简单,但sign、version、timestamp这些控制字段没地方放,硬塞进payload里又不方便服务端先做验签再解密。统一结构更利于排查问题,各字段职责单一。

3. 完整实现:基于Spring Boot的请求解密与响应加密

3.1 核心思路:统一拦截,业务无感

方案定完后,落地方式很关键。我不希望业务代码里到处调CryptoUtil.decrypt,那会让Controller层乱成一锅粥。正确做法是把加解密放到Servlet层面的Filter里做,业务Controller拿到的直接就是解密后的POJO。

我选Filter而不是Spring Interceptor,原因有两点:一是Filter在Spring MVC处理链之前,能最原始地拿到请求体,能处理请求体缓存问题;二是响应流也可以在这个层面被包装和改写,Interceptor想改写响应体很别扭,往往得再搞一层ResponseBodyAdvice。Filter一个位置把入参解密和出参加密全搞定,逻辑最简单。

整个流程是这样:加一个自定义Filter,在doFilter里读取原始请求体,判断当前接口是否需要加解密(通过路径规则或注解),需要就走解密逻辑,把解密后的内容重新包装成Request,继续放行;响应回来时对输出流做包装,待Controller写入内容后统一加密再交给客户端。

3.2 加解密工具类实现

先写核心工具类。这个类负责AES-GCM加解密、RSA加解密、HMAC签名三块功能。为了不引入额外依赖,全部用JDK自带的API。

import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.Base64; public class CryptoUtil { private static final int GCM_TAG_LENGTH_BITS = 128; private static final int AES_KEY_SIZE_BITS = 256; // 生成AES会话密钥 public static byte[] generateAesKey() throws Exception { KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(AES_KEY_SIZE_BITS); return generator.generateKey().getEncoded(); } // AES-GCM加密:data为明文,key为AES密钥,返回 iv+密文+tag 的合并结果 public static byte[] aesGcmEncrypt(byte[] data, byte[] key) throws Exception { // 12字节随机IV,GCM推荐IV长度 byte[] iv = new byte[12]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] cipherText = cipher.doFinal(data); // 拼接:iv + 密文 byte[] result = new byte[iv.length + cipherText.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(cipherText, 0, result, iv.length, cipherText.length); return result; } // AES-GCM解密:入参为 iv+密文+tag 的合并结果 public static byte[] aesGcmDecrypt(byte[] encryptedData, byte[] key) throws Exception { byte[] iv = new byte[12]; System.arraycopy(encryptedData, 0, iv, 0, iv.length); byte[] cipherText = new byte[encryptedData.length - iv.length]; System.arraycopy(encryptedData, iv.length, cipherText, 0, cipherText.length); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); return cipher.doFinal(cipherText); } // RSA公钥加密,OAEP填充 public static byte[] rsaEncrypt(byte[] data, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(data); } // RSA私钥解密 public static byte[] rsaDecrypt(byte[] data, PrivateKey privateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(data); } // HMAC-SHA256签名 public static String hmacSign(String data, byte[] key) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256"); mac.init(keySpec); byte[] result = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(result); } }

这里有几个细节要特别注意。第一,GCM的tag长度我固定用128位,这是安全性和性能的平衡点。第二,AES/GCM/NoPadding里写NoPadding是标准写法,GCM模式自带认证逻辑,不是真正不做填充。第三,RSA加密时用OAEP填充而不用PKCS1,OAEP更安全,能避免部分已知的格式攻击。

3.3 请求解密与响应加密的Filter实现

Filter这块分为两步:请求解密和响应加密。先看请求方向。

@Component public class ApiCryptoFilter extends OncePerRequestFilter { private final PrivateKey privateKey; // 省略密钥注入 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 只有需要加解密的接口才处理 if (!shouldDecrypt(request)) { chain.doFilter(request, response); return; } // 读取原始body,只允许读一次 byte[] requestBody = StreamUtils.copyToByteArray(request.getInputStream()); if (requestBody.length == 0) { chain.doFilter(request, response); return; } try { // 解析统一的请求结构 RequestEnvelope envelope = objectMapper.readValue(requestBody, RequestEnvelope.class); // 校验签名和时间戳,防止篡改和重放 if (!verifySign(envelope) || !checkTimestamp(envelope.getTimestamp())) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write("{\"code\":403,\"msg\":\"invalid sign or expired\"}"); return; } // 1. RSA解出AES会话密钥 byte[] aesKey = CryptoUtil.rsaDecrypt( Base64.getDecoder().decode(envelope.getEncKey()), privateKey); // 2. AES-GCM解密payload byte[] plainData = CryptoUtil.aesGcmDecrypt( Base64.getDecoder().decode(envelope.getPayload()), aesKey); // 3. 将解密后的内容包装为新的Request HttpServletRequest wrappedRequest = new RequestWrapper(request, plainData); chain.doFilter(wrappedRequest, response); } catch (Exception e) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write("{\"code\":400,\"msg\":\"decrypt failed\"}"); } } }

请求体读取是个容易踩坑的地方。request.getInputStream()只能读一次,如果Filter前面还有其他组件读过Body,后面Controller再读就会拿到空流。所以要么在Filter链的最前面处理,要么用Spring提供的ContentCachingRequestWrapper先把内容缓存下来。我这边是自定义Filter并确保它在链的最前面,同时自己实现一个RequestWrapper来缓存明文body,Controller再用@RequestBody读取时拿到的就是解密后的内容。

响应方向的实现逻辑对称,核心是包装HttpServletResponse的输出流,在Controller执行完、数据写出时拦截下来加密。

// 响应包装类,拿到Controller写入的内容并加密 public class ResponseWrapper extends HttpServletResponseWrapper { private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); public ResponseWrapper(HttpServletResponse response) { super(response); } @Override public ServletOutputStream getOutputStream() { return new ServletOutputStream() { @Override public void write(int b) { buffer.write(b); } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener listener) {} }; } @Override public PrintWriter getWriter() { return new PrintWriter(buffer); } public byte[] getBytes() { return buffer.toByteArray(); } }

在Filter末尾调用chain.doFilter后,从ResponseWrapper里取出明文内容,用会话密钥加密后,塞回真正的response流。有个细节:必须在业务线程完全返回后处理,否则拿不到完整数据。用OncePerRequestFilter加自定义包装类,这个顺序天然是保证的。

3.4 前端配合的几个关键点

这个方案必须前后端一起改。前端拿到RSA公钥后,逻辑是固定的:

  1. 生成随机AES会话密钥(H5端可用Web Crypto API,App端用系统自带的加密库,不要自己写随机数)。
  2. 对明文payload做AES-GCM加密。
  3. 用RSA公钥加密AES会话密钥。
  4. 组装统一结构,发送请求。
  5. 收到响应后,用同一个AES密钥解密。

前端最常犯的错误是Base64编码不一致。Java默认的Base64标准编码和解码都是线程安全的,但H5端要注意URL安全的Base64和标准Base64不是一回事。我建议前后端统一使用“标准Base64,不换行”,并且字符集统一UTF-8,能规避掉90%的乱码问题。

另外一个很实际的点:调试期怎么办。总不能前端每次联调都要先生成密钥、加密报文再发请求吧?我这边做了一个开关接口,测试环境可以直接明文请求,响应也明文返回,只有线上环境强制加密。开关配置在服务端,和接口的加解密开关绑定:

api: security: crypto-enabled: true # 线上真加密 # debug: false # 本地调测时改为false,直接透传

这个开关的粒度必须到接口级别,而不是全局。有的接口(比如健康检查、静态配置)根本不需要加密,全局加密会把简单事情复杂化。我是用注解加路径白名单双层控制,注解优先。

4. 踩坑实录与排查技巧

4.1 常见问题速查表

现象大概率原因处理方式
解密后中文乱码字符集不一致,服务端用GBK而前端用UTF-8统一指定UTF-8,并在响应头显式声明
报错Tag mismatchAES-GCM解密失败,说明密钥不对或密文被改动核对AES密钥是否一致,检查payload是否被截断
报错BadPaddingExceptionRSA解密失败,通常公钥私钥不匹配检查密钥对版本,是否重新生成过
请求体为nullFilter提前读了InputStream用ContentCachingRequestWrapper缓存,或自己实现RequestWrapper
响应体为空ResponseWrapper没获取到完整数据确认在chain.doFilter返回后再加密输出
Base64解码异常前端用了URLSafe编码或加了换行统一标准Base64,编码前去除换行
Controller拿到的还是密文Filter顺序错了,Spring MVC先执行了将ApiCryptoFilter注册到FilterRegistrationBean的最前位
时间戳校验老失败客户端时间不准,或误差设置太小允许前后5分钟偏移,并校准客户端时间

这里最大的坑是RSA解密BadPaddingException。我当时排查了整整一个下午,最后发现是有个环境变量里配置的RSA密钥对和服务端实际加载的不是同一对。这种问题最隐蔽,因为代码、算法、格式全对,就是密钥不是同一对。后来我统一把密钥版本号加到请求结构里,客户端拿着哪个版本的公钥加密,服务端就用对应版本的私钥解密,再没出过这种问题。

4.2 中文乱码和Base64的细节

中文乱码看着简单,实际上一旦出现就是连环问题。我遇到过的情况是:AES解密后字节数组是好的,但转字符串时用了平台默认编码。Linux服务器上默认一般是UTF-8,Windows开发机默认是GBK,同一段代码在开发环境正常,上线就乱码。我给自己定了个铁律:所有涉及字节和字符串转换的地方,显式指定StandardCharsets.UTF_8,绝不依赖系统默认。

Base64方面,我要提醒Base64.getEncoder()默认输出会有换行符吗?JDK的Base64.getEncoder()是不换行的,但有些类库如android.util.Base64默认开启换行,Java服务端解析时会遇到IllegalArgumentException。所以我在协议里明确要求:所有Base64字符串必须是“单行、无换行、标准字符集”。

4.3 调试期怎么优雅地“关掉”加密

我在前面提到了开关机制,这里展开讲讲。crypto-enabled这个开关,在测试环境设为false时,Filter直接透传,不上加密逻辑,联调和本地写单测都会很舒服。但要注意,不能只靠一个开关就完事,还要处理好“部分接口加密、部分接口不加密”的过渡期。

我常用的做法是:每个接口在注解里声明是否需要加解密,比如@ApiSecurity(encrypt = true)。Filter处理时先看注解,注解没标就按配置决定。这样老接口可以逐步迁移,新接口默认强制加密,灰度发布时只放开影响面小的几个接口,不会一把梭。

线上排查问题时会遇到一个尴尬的场景:加密接口出故障了,想用curl模拟请求都做不到,得写个小工具生成密文。我建议提前写一个命令行小工具,输入明文和公钥,直接输出加密后的完整报文。排查问题时能省很多时间。

4.4 性能与日志脱敏

很多人担心加解密拖垮接口性能,我实测下来,AES-GCM在有硬件加速的CPU上非常快,即使普通云服务器,加密1KB数据耗时也就在微秒到毫秒级别。真正有开销的是RSA非对称加密,一次RSA-2048加密大概在几毫秒到十几毫秒,但我们的设计里RSA只加密一个32字节的AES密钥,而不是整个报文,所以整体开销完全可以接受。

性能优化的两个建议:一是如果接口QPS很高,可以缓存公钥和私钥对象,避免每次解密都从文件或ClassPath重新加载;二是如果对延迟极度敏感,考虑服务端下发AES密钥时用长会话模式,客户端在会话有效期内复用同一个AES密钥,减少RSA加解密的次数。但要注意长会话会降低密钥安全性,建议控制有效时间在10分钟以内,并且和nonce校验结合使用。

日志脱敏方面,我有一个血泪教训:Filter里解密的密文和明文,任何一层都不要打印。尤其是明文的payload,一旦打到日志里面,前面所有加密等于白做。我有次排查问题时顺手在Filter里加了一行日志,把解密后的请求体打了出去,上线一周后安全扫描就报了敏感数据泄露。后来我专门在日志规范里加了一条:含有手机号、身份证、银行卡、token等字段的日志,一律掩码处理;密文长度超过128位的body,不允许整体打印,只能打印长度和MD5摘要。

5. 从“能用”到“好用”的经验沉淀

5.1 密钥管理与轮换

密钥管理是最容易被忽略但又最关键的一环。我见过有些团队把RSA私钥直接放在代码仓库里,还写死在常量类里,这基本等于把钥匙挂在门锁旁边。

我的做法是RSA私钥和HMAC签名密钥放在环境变量或配置中心,不进入代码仓库。每个环境用单独一套密钥,测试、预发、生产互不相同。密钥版本号放到统一请求结构里,线上切换密钥时,保留上一版本私钥继续解密一段时间,等客户端都切到新版本后,再下线旧密钥。这个“双版本并行”的窗口期一般控制在3到7天。

AES会话密钥是每次请求临时生成的,不需要持久化,用完就丢。真正需要运维的是RSA密钥对和HMAC密钥,建议至少每三个月轮换一次。轮换时先发新公钥给客户端,等客户端完成切换后,再把旧私钥停用。整个过程尽量在低峰期操作,并且提前准备好回滚预案——万一客户端有兼容性问题,能快速切回旧版本。

5.2 别只做加解密,纵深防御要配套

应用层加解密只是接口安全的一个环节,它解决的是“看得懂、改不了、赖不掉”的问题,但挡不住所有攻击。我建议在加解密之外,把下面几件事一起做:

  • 防重放:时间戳+nonce,配合服务端nonce缓存,在有效期内重复使用的nonce直接拒绝
  • 防篡改:HMAC签名覆盖payload、时间戳、nonce和version,任何字段变动都验签失败
  • 防弱口令:AES密钥必须真随机,严禁用固定字符串或UUID做种子
  • 输入校验:解密后的数据仍然要做参数校验、长度限制、白名单校验,不能因为“已经解密了”就放松

另外我还会在网关层做限流和风控,比如同一IP单位时间内访问次数超过阈值就临时封禁。加密和风控是两条线,加密解决数据安全,风控解决滥用行为,两者缺一不可。

5.3 这个方案还能怎么扩展

如果后续要接入更多客户端类型,可以把加解密能力沉淀成一个独立的SDK,前端、App、后端统一引用同一个版本的SDK,避免各端算法实现不一致导致互调失败。

如果是微服务架构,可以考虑把加解密能力下沉到网关层,在网关统一做RSA、AES交换,下游服务只拿到解密后的明文,这样下游服务无感知,接入成本最低。但对下游服务来说,数据在服务内部走的是明文,内部链路的安全性要靠其他手段补足,比如服务间调用单独做服务间认证和通信加密。

如果业务量再大,可以上专用的密钥管理服务来做密钥的存储和轮换,替代手工管理密钥对的方式。不过这也意味着要引入新的依赖项和运维成本,需要权衡。

6. 最后分享一些实操体会

我在实际项目中踩过几次坑之后总结的经验是:加解密方案真正难的点从来不是“会不会写AES”,而是“密钥怎么管、兼容怎么做、排查怎么搞”。

密钥管理做不好,再标准的算法也会在灰度发布时炸掉;兼容设计做不好,前端老版本用户直接请求失败,比不加密还麻烦;排查工具没准备,线上出问题只能干瞪眼。

我个人强烈建议动手之前先写一页技术方案,把算法选型、报文格式、密钥生命周期、调试开关、排查工具五件事列清楚,再让前后端同学评审一遍。方案定了以后再写代码,会顺畅很多。

这个内容后续还可以这样扩展:做一个自动生成“加密报文”的命令行工具,再把接口加解密状态做成仪表盘监控,看看哪些接口还没接入、哪些接口加解密耗时异常。等整个体系跑一段时间,你会明显感觉到接口层面的数据裸露少了很多,心里踏实不少。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进