ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot接口加解密实战:RSA+AES+签名验签与等保合规方案

Spring Boot接口加解密实战:RSA+AES+签名验签与等保合规方案 让你从头写一套接口加解密方案可能大家第一反应都是“直接上全链路HTTPS不就行了”。但你要是真拿这种思路去做等保2.0的整改等测评师拿抓包工具一看发现业务请求体里的明文内容一眼就能读完那你连整改说明都写不踏实。更实际的情况是很多项目根本不具备全程私有网络的部署条件第三方对接、移动端直连、H5页面调用这些场景下接口对外暴露几乎是必然的。所以我在实际做等保合规改造时最终选择了一套很经典的组合方案RSA AES 签名验签用RSA做密钥的交换用AES做业务数据的加解密再用签名保证数据没有被中途篡改。整套逻辑放在Spring Boot 3.4里实现既能满足合规审计的“传输加密、完整性保护、抗重放”这些通用安全基线又能真正落地到日常业务接口不阻塞开发联调。这篇文章我会把从方案选型、报文协议设计、工具类封装到Filter拦截器、响应加密、防重放、异常分支处理再到线上压测数据和踩坑经验一次性讲完。适合正在做等保整改的项目负责人、主力后端以及想了解接口安全问题解决方案的初中级开发者。内容不会只贴代码我会把每一步“为什么这么设计”也说清楚这样你拿去评审、写设计文档、对接第三方时才不会被问倒。1. 等保合规的通用安全基线接口防泄露与防篡改到底考什么每次一提到等保2.0很多人脑子里冒出来的就是防火墙、堡垒机、日志审计、密码设备这些大件。业务接口层面的加解密反而容易被糊弄过去。但我做过几个等保整改项目之后有个很直观的感受测评最常抓的接口漏洞其实就是明文传输、无防篡改机制、无抗重放能力这三样。1.1 安全通信网络要求里真正落地到接口的条款通读等保2.0里跟应用层相关的安全要求扣住核心有这么几条通信传输完整性应用系统应该能检测到通信过程中数据是否被篡改。通信传输保密性应用系统应该能对通信过程中的敏感数据字段进行加密。抗重放应能防止在通信过程中的重复报文体被反复提交。可信验证应基于硬件级可信根对通信设备的引导程序等进行可信验证这部分往往落地到服务器侧更多。翻译成你能直接写代码的功能点就是合规需求技术实现落地角色传输保密性请求体/响应体应用层AES加密服务端过滤器 客户端SDK传输完整性对报文关键字段做RSA签名验签服务端验签逻辑抗重放时间戳窗口 随机nonce唯一性判断拦截器/过滤器审计追溯加解密状态日志、验签失败告警日志切面这套需求并不会死抠“你必须用RSA还是SM2”更不会指定“你必须用AES-256-GCM”。它给的是一个目标不是实现路径。所以你完全可以做技术选型只要能把完整性和保密性的面儿撑起来测评师看的是测评结果不是你的具体实现函数名。1.2 为什么只上HTTPS还不够用实测中很多开发对“加密”的理解就是“上了HTTPS就安全了”。这话不能算全错但放到等保整改里明显不够。HTTPS解决的是TLS层传输加密它保护的是“中间人看不到你TCP管道里的内容”。可问题在于你一旦把接口暴露给第三方或H5页面业务体到达终端以后、进入TLS加密之前以及离开TLS解密之后都存在明文窗口期。更直接的情况是调用方拿着抓包工具在App端用中间人代理把HTTPS证书替换了这方面工具太成熟了照样能直接看到明文JSON。哪怕你配了HTTPS业务字段还是会以明文形式出现在请求Body里。所以我把应用层加解密称之为“最后一段私密性兜底”。它不做传输层的事只负责保证就算数据被扒下来也是一堆密文和签名没法直接伪造合法报文、没法直接阅读业务内容。这套思路在等保里也更好过关因为“应用系统设计为敏感数据字段加密存储和传输”是能明确写进整改说明书的。1.3 加解密方案为什么是RSAAES签名三件套先说结论纯RSA加密所有业务数据完全不可行纯AES对称加密没有密钥安全管理能力只验签不加密则隐私内容暴露只加密不验签则可能被伪造密文投毒。实际职责划分很简单RSA负责密钥交换客户端随机生成一个AES对称密钥用服务端公钥加密后传给服务端服务端用私钥解开拿到AES密钥。AES负责数据机密性真正加密大段业务JSON用AES吞吐高、性能好、没有RSA那种“最多只能加密密钥长度减掉填充余量”的硬限制。RSA签名负责完整性用私钥对明文业务字段签名接收方用公钥验签只要数据被改动一个字节签名就对不上。这里有个关键感悟不要在业务体加密之外去信任任何“看起来没被动过的JSON”。哪怕外面包了层AES密文如果对方AES密钥已经被拖走密文照样可伪造。所以签名要覆盖“解密后的明文 时间戳 nonce随机数”而不是只签一个密文摘要。2. 报文协议设计整个方案的地基写代码之前第一步不是建项目而是定协议。协议不先想明白后面写Filter会乱成一锅粥。我给出的报文格式是我在项目中踩过坑之后调整出来的版本兼顾了可读性和安全性。2.1 请求报文格式与字段职责分配客户端发给服务端的JSON统一这种结构{ encryptKey: base64(RSA公钥加密后的AES密钥), timestamp: 1733721600000, nonce: f3a4b7c9-6d8e-4f1a-9c2b-5e7d8a9b0c1d, sign: base64(SHA256withRSA签名字符串), data: base64(AES-GCM加密后的业务数据) }我做项目时习惯把encryptKey、timestamp、nonce放外层命名为“固定包头字段”data是真正的业务密文。服务端处理顺序是拿到encryptKey用服务端私钥解密得到AES对称密钥。用AES密钥解密data字段拿到明文业务JSON。用明文业务JSON timestamp nonce 拼出待验签串再用客户端公钥验sign。验签通过后放到Request属性里继续走Spring MVC正常流程。顺序很重要AES解密必须在验签之前做因为签名针对的是明文业务体不是密文。这个顺序你要是反过来就永远验不过。2.2 先签后加密与先加密后签名的选择有的方案喜欢把签名放在密文外面也就是先把业务体用AES加密再对密文本身做RSA签名。这样做的好处是服务端不用先解密就能验签能省一次解密流程。坏处也很明显签名无法覆盖到明文内容万一某个环节把密文解密之后又用了另一套明文那后面再怎么验签都验的是密文对业务明文没有约束力。我倾向于“先组装完整明文签名明文再加密明文”。也就是sign本身也是基于明文生成的。服务端验签时先解密拿到明文再验签。这里的额外开销也就是一次AES解密这对现代CPU来说几乎可以忽略但换来的却是“签名直接关联真实业务语义”的强保证。等保评审讲“完整性校验”时这个设计讲起来逻辑也顺。2.3 时间戳和nonce的防重放配合防重放这块最容易踩坑的就是只做时间戳判断。比如你判断“请求时间不超过当前时间5分钟”那攻击者在这5分钟内反复重放同样的请求你的业务系统依然会重复执行。尤其是下单、转账、领券这类操作重放就是真金白银的事。所以协议里nonce不是摆设它的目的就是配合Redis或本地缓存实现“一次性请求ID”。服务端对每个已处理过的nonce缓存一段时间比如10分钟缓存有效期内如果再次提交相同的nonce直接拒绝。时间戳管“过期”nonce管“去重”两者配合才能把重放漏洞堵死。实现中我建议用Redis而不是本地Map做nonce去重因为接口往往是多实例部署本地Map在集群里根本不共享只有Redis或集中式缓存才能保证全局唯一。代码大概是String nonceKey API:NONCE: nonce; Boolean firstSeen redisTemplate.opsForValue().setIfAbsent(nonceKey, 1, Duration.ofMinutes(10)); if (Boolean.FALSE.equals(firstSeen)) { throw new ApiSecurityException(重复请求拒绝处理); }别小看这段代码它就是我线上联调时被坑出来的经验。第一版我图省事只做了时间戳结果测试自测时用jmeter重放了10个一模一样的下单请求库存直接被扣了10次。3. Spring Boot 3.4工程化基础工具类与密钥管理接下来开始写代码。我用的是当前比较新的Spring Boot 3.4.x JDK 17。如果你还在用Spring Boot 2.x有些类名和API会有差异比如jakarta.servlet和javax.servlet的区别这个需要你自己迁移一下。3.1 项目依赖与基础配置新建项目时引入这几个依赖就够了dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency如果你本地没起Redis可以先不管nonce去重那部分逻辑或者直接用ConcurrentHashMap做单机版降级但上线前请务必换回Redis。application.yml里配置公私钥和加密方案参数api-security: server-private-key: MIIEvQIBADANBg...完整Base64私钥 client-public-key: MIIBIjANBgkq...完整Base64公钥 aes-key-size: 256 timestamp-valid-window: 300000注意server-private-key是服务端私钥用来解客户端传过来的加密AES密钥client-public-key是客户端公钥用来验证客户端签名。有些团队习惯反过来服务端私钥签名、客户端公钥验签这就要看你们密钥是双向还是单向的。我写的是“客户端签名、服务端验签、服务端加密响应、客户端解密响应”的模型更贴近真实第三方对接。3.2 RSA工具类算法与填充模式的选择RSA工具类里最容易掉坑的是填充模式。网上很多示例写的是RSA/ECB/PKCS1Padding这个放在OAEP出现前的老系统里还能跑但现在安全测评看到PKCS1Padding基本都会挑毛病要求升级到OAEP。Java里实际推荐用RSA/ECB/OAEPWithSHA-256AndMGF1Padding也就是OAEP的SHA-256变体。它比老版PKCS1多了一次掩码生成抗攻击能力更强。public class RsaUtil { private static final String RSA_ALGORITHM RSA/ECB/OAEPWithSHA-256AndMGF1Padding; private static final int KEY_SIZE 2048; public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator generator KeyPairGenerator.getInstance(RSA); generator.initialize(KEY_SIZE); return generator.generateKeyPair(); } public static String encrypt(String plainText, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception { Cipher cipher Cipher.getInstance(RSA_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decrypted cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, StandardCharsets.UTF_8); } }这里有一个容易被忽视的硬限制RSA一次只能加密“密钥位数 / 8 - 2 * 哈希长度 - 2”那么长的数据。用2048位密钥、SHA-256摘要的话最多只能加密约190个字节左右。所以千万不要拿RSA去加密长JSONAES密钥本身才32字节加密它完全没问题。3.3 AES工具类为什么用GCM而不是ECB或CBCAES本身有很多工作模式我只推荐GCM。原因很直白ECB模式相同明文加密成相同密文根本不隐藏任何重复模式测评当场就能指出来。CBC模式需要初始化向量还需要自己拼PKCS5Padding容易做错。GCM模式自带认证标签能同时保证机密性和完整性Java底层直接支持。使用GCM时需要两个重要参数一个12字节的初始化向量IV和一个16字节的认证标签。每次加密都必须生成新的随机IV绝对不能复用。代码里我会把IV和密文拼在一起传输方便解密时提取public class AesGcmUtil { private static final int GCM_IV_LENGTH 12; private static final int GCM_TAG_LENGTH_BITS 128; public static EncryptResult encrypt(byte[] plainText, byte[] aesKey) throws Exception { byte[] iv new byte[GCM_IV_LENGTH]; SecureRandom secureRandom new SecureRandom(); secureRandom.nextBytes(iv); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec keySpec new SecretKeySpec(aesKey, AES); GCMParameterSpec gcmSpec new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] cipherText cipher.doFinal(plainText); byte[] combined ByteBuffer.allocate(iv.length cipherText.length) .put(iv) .put(cipherText) .array(); return new EncryptResult(combined, iv); } public static byte[] decrypt(byte[] combined, byte[] aesKey) throws Exception { byte[] iv Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH); byte[] cipherText Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec keySpec new SecretKeySpec(aesKey, AES); GCMParameterSpec gcmSpec new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); return cipher.doFinal(cipherText); } }注意SecureRandom必须每次重新取不能用new Random()否则IV生成不够随机GCM的认证保护形同虚设。3.4 签名工具类私钥签名公钥验签我偏好用SHA256withRSA做签名算法。它基于非对称密钥客户端和服务端各持有一对签字方用私钥签名验证方用公钥验证天然符合“防抵赖”的需求。如果你更偏好国密算法也可以换成SM2withSM3不过那样要额外引入BouncyCastle这里先用标准JDK能跑的方案演示。public class SignatureUtil { public static String sign(String content, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(content.getBytes(StandardCharsets.UTF_8)); byte[] signed signature.sign(); return Base64.getEncoder().encodeToString(signed); } public static boolean verify(String content, String signatureBase64, PublicKey publicKey) throws Exception { Signature signature Signature.getInstance(SHA256withRSA); signature.initVerify(publicKey); signature.update(content.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.getDecoder().decode(signatureBase64)); } }签名内容我建议统一用一个规范化拼串方法把参数按固定顺序拼起来避免两边字段顺序不一致导致验签失败。比如String signContent plainBusinessJson timestamp nonce;时间戳、nonce一定要参与签名计算。如果只签明文业务体攻击者可以把一个老请求里的timestamp改成不超时的新时间只要业务体没变签名照样是有效的这会绕开重放校验。4. 核心实战环节过滤器解密请求、响应加密工具类写完之后真正难的是把它们串进Spring Boot的请求处理链路里。这里要说明我对拦截器选择的思考过程。4.1 为什么用Filter而不是InterceptorSpring MVC的HandlerInterceptor虽然更好用但它工作在Spring MVC的HandlerMapping之后。如果有请求压根没映射到任何Controller比如404那Interceptor根本不会执行。更麻烦的是如果把解密逻辑放在Interceptor里等你看到HttpServletRequest的时候Spring MVC已经因为找不到合适的方法而报错了。Filter处于Servlet容器最外层请求进入容器后最先经过Filter链还没碰Spring MVC。这样我可以对请求体完成“解密 验签”之后再把包装过的请求对象传递下去。后续不管Spring MVC能不能找到Controller至少加解密逻辑是完整生效的。4.2 RequestWrapper的写法与流的缓存HttpServletRequest.getInputStream()只能读一次这是Servlet规范限制。而Filter里解密必然要读一遍输入流解密之后还得让Controller再读一遍解密后的明文流所以必须做一层包装。我的做法是自定义一个SecurityRequestWrapper在构造时就把原始流读出来、解析并解密之后Controller读到的是解密后的明文流public class SecurityRequestWrapper extends HttpServletRequestWrapper { private final String decryptedBody; public SecurityRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 读取原始请求体 String encryptedBody StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 解析报文头 SecurityRequestPacket packet JSON.parseObject(encryptedBody, SecurityRequestPacket.class); String aesKeyBase64 RsaUtil.decrypt(packet.getEncryptKey(), serverPrivateKey); byte[] aesKey Base64.getDecoder().decode(aesKeyBase64); byte[] decryptedBytes AesGcmUtil.decrypt(Base64.getDecoder().decode(packet.getData()), aesKey); String plainBody new String(decryptedBytes, StandardCharsets.UTF_8); // 验证签名 String signContent plainBody packet.getTimestamp() packet.getNonce(); boolean valid SignatureUtil.verify(signContent, packet.getSign(), clientPublicKey); if (!valid) { throw new ApiSecurityException(验签失败数据可能被篡改); } // 保存解密后的明文供后续业务使用 this.decryptedBody plainBody; // 顺便缓存AES密钥响应加密时复用 request.setAttribute(Constants.AES_KEY_ATTR, aesKey); } Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream new ByteArrayInputStream(decryptedBody.getBytes(StandardCharsets.UTF_8)); return new ServletInputStream() { Override public int read() throws IOException { return byteArrayInputStream.read(); } Override public boolean isFinished() { return byteArrayInputStream.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener readListener) { } }; } Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } }这里有个很重要的点我把AES密钥塞进了Request的attribute里。这样后续响应加密时就能直接用request.getAttribute(Constants.AES_KEY_ATTR)取出同一把会话密钥保证响应是能用这把密钥解密的。4.3 响应加密的实现路径ResponseBodyAdvice vs 包装器响应加密目前有两种主流做法ResponseBodyAdvice在Spring MVC序列化Controller返回值之后、写回响应体之前拦截对JSON字符串做AES加密。优点是能拿到真实的Controller返回对象改起来很顺手缺点是它是Spring MVC里的一环如果请求根本没进Controller比如Filter里直接抛了异常它拦不到。自定义HttpServletResponseWrapper在Filter里把响应包装成“只写密文”的输出流在Controller返回时把所有字节缓存下来最后统一加密。优点是覆盖链路更完整连Spring Security的406、401响应都能处理缺点是写起来麻烦要自己维护一个BufferOutputStream。如果你背景是纯Spring Boot没有Spring Security我建议用ResponseBodyAdvice开发效率更高。如果你们项目里同时接了Spring Security或Shiro最好用Filter ResponseWrapper。我用Filter全链路包装的方案做演示因为更贴近等保整改“全部响应密文化”的诉求。public class SecurityResponseWrapper extends HttpServletResponseWrapper { private final ByteArrayOutputStream buffer new ByteArrayOutputStream(); public SecurityResponseWrapper(HttpServletResponse response) { super(response); } Override public ServletOutputStream getOutputStream() throws IOException { return new ServletOutputStream() { Override public void write(int b) throws IOException { buffer.write(b); } Override public boolean isReady() { return true; } Override public void setWriteListener(WriteListener writeListener) { } }; } Override public PrintWriter getWriter() throws IOException { return new PrintWriter(buffer, true); } public byte[] getBufferedBytes() { return buffer.toByteArray(); } }Filter主逻辑里在chain.doFilter执行完之后从包装器里取出明文字节拿Request里缓存的AES密钥加密再往真实Response里写出Slf4j Component public class ApiSecurityFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) servletRequest; HttpServletResponse response (HttpServletResponse) servletResponse; String uri request.getRequestURI(); if (shouldSkip(uri)) { chain.doFilter(request, response); return; } SecurityRequestWrapper requestWrapper; SecurityResponseWrapper responseWrapper new SecurityResponseWrapper(response); try { requestWrapper new SecurityRequestWrapper(request); } catch (ApiSecurityException e) { // 这里要写一个统一异常响应注意也要加密否则就留了明文逃生口 writeErrorResponse(response, e); return; } try { chain.doFilter(requestWrapper, responseWrapper); byte[] plainBytes responseWrapper.getBufferedBytes(); byte[] aesKey (byte[]) requestWrapper.getAttribute(Constants.AES_KEY_ATTR); byte[] encrypted AesGcmUtil.encrypt(plainBytes, aesKey); response.getOutputStream().write(encrypted); } catch (Exception e) { writeErrorResponse(response, e); } } }4.4 签名验签流程与异常处理分支我在Filter里会把验签失败、解密异常、重复请求分情况抛出不同的错误码。比如验签失败返回4101重复请求返回4102解密失败返回4103。错误响应也要遵循统一密文封装否则攻击者能拿明文错误信息做信息收集。错误响应的格式和正常响应一致{ timestamp: 1733721600000, nonce: 随机值, sign: 签名, data: AES密文里面的业务结构是 {code:4101, message:验签失败} }签名就用服务端的私钥对错误信息明文做一次。客户端拿到后先验签再解密保证错误信息也是可信的、未经篡改的。5. 上线前必须知道的坑从开发到联调的灵魂拷问这一章节全是实战里被捶出来的经验不写代码但比代码更值钱。5.1 流只能读一次引发的血案有次联调时对方一直报“请求体为空”排查半天发现是我们在Filter里读了一次原始输入流解密完用getReader()给Controller提供流但另一个权限过滤器又先一步读了getInputStream()把流读完没重置。后面Spring MVC再读就只剩空数据了。解决办法有两个一是建立团队约定所有需要读Body的过滤器都基于自定义Wrapper来做不直接操作原始Request二是在Wrapper里做一次缓存让后续所有getInputStream()调用都返回同一个缓存流不要各自去原始流里读。5.2 Spring Security和Swagger的链路冲突集成Spring Security时过滤器顺序会变成一个真正的坑。SecurityFilter一定要在ApiSecurityFilter之后吗不一定要看你的鉴权逻辑需不需要明文。如果Spring Security的认证过滤器需要读取用户名密码做登录校验那它必须在解密之后执行。而Swagger文档的访问路径/v3/api-docs/**、/swagger-ui/**又必须跳过加解密否则没带密钥的文档请求会直接被拦死。我最终的过滤器链跑出来大致是这样ApiSecurityFilter放在最外层但内部用白名单跳过Swagger、健康检查、静态资源然后才是Spring Security。如果你在Filter里做加解密同时Spring Security又做认证那HttpServletRequest必须用包装器传给Spring Security否则Security读不到明文。5.3 异步返回与响应加密失效问题如果Controller用了Callable或DeferredResult请求会提前返回等异步线程处理完后再提交响应。这时候Filter里的doFilter已经执行完了responseWrapper.getBufferedBytes()拿到的可能是空内容异步线程里的输出流根本不会经过你的加密逻辑。处理方式要么约定所有异步接口结果先统一收集再走一次包装器要么干脆规范项目里回调请求都走同步不搞异步。第二个方案听起来粗暴但实际项目里业务接口大部分都是同步的硬上异步只会让加解密链路的复杂度飙升。5.4 日志打印与敏感数据脱敏加解密日志这块很容易在等保审计时被翻出来砍一刀。明文业务日志里如果打到身份证、手机号、地址那测评直接视为敏感信息泄露。我建议日志里只打印加密前报文的哈希值、请求路径、时间戳、nonce。明文业务体一律不输出除非开了debug模式且只输出到本地文件。异常堆栈打印时注意别把请求参数原样打印。这四条当时帮我挡住了好几条测评整改项确实值得记下来。6. 性能实测与上线验收这把锁到底上多少成本很多人一听到“接口加解密”就觉得性能会崩。实际上用GCM对称加密开销远没想象中那么大。真正贵的反而是签名验签里的一次RSA非对称运算和双方密钥交换。6.1 压测数据加解密带来的耗时开销占比我在一台低配4核8G的虚拟机上做过压测测试场景是Spring Boot 3.4 Redis单接口平均处理逻辑本身约40ms分成三组对比场景未加密加解密验签加解密验签Redis去重QPS约约 860约 640约 600TP99响应耗时58ms78ms82ms平均耗时增量基准约 15%约 20%可以看出加解密链路对性能的影响大概在15%到20%业务逻辑本身也许只有几十毫秒加解密多出来的其实不算离谱。关键还是看你怎么优化。6.2 可优化的四个方向公钥缓存、密钥池化、并行加密、降低日志公钥缓存从配置里加载公钥、私钥时不要每次都重新解析PKCS8而是用单例对象缓存住PrivateKey和PublicKey。RSA私钥解析在Java里是个耗时的操作每次请求都做的QPS能立刻掉一截。AES密钥池化虽然这笔实现里是每次请求随机生成一把AES密钥但如果你追求更高性能可以考虑密钥池化让一段时间内的短会话复用同一把对称密钥减少RSA交换次数。并行加密如果响应体特别大可以分段做GCM加密Java的Cipher支持并行更新能利用多核优势。但业务响应通常不大优化收益不明显。日志级别把控加解密失败时容易打印大量堆栈日志I/O在压测时是隐形杀手生产环境务必把异常日志量控制在合理范围内。6.3 最终上线检查清单和验收建议我在交付每个项目前会跑一遍自查清单这里公开给你参考白名单路径是否已配置完整Swagger、健康检查、静态资源是否不受影响。是否已确认所有对外业务接口都被加密拦住没有漏网的明文接口。nonce去重是否已接入Redis长时间运行的集群不会出现本地Map分片去重失效。错误响应是否也做了加密没有把异常明文写给调用方。日志里是否还有明文敏感字段输出。客户端SDK是否覆盖了生成AES密钥、RSA加密、AES加密、签名生成的全流程。密钥轮换机制是否已准备至少约定好运维侧半年到一年换一次公私钥。每个小项有问题的补完再提测。最好在预评审前自己先用抓包工具模拟一遍攻击互传一个明文包、修改一个字节试试看看服务端能不能正确拦截。我自己的经验是这类安全改造最耗时的不是代码实现而是跟不同调用方对协议、排错、联调的过程。所以你一点要把报文格式、错误码、签名规则写成一份对接文档提前发给所有外部调用团队省下后面在线上一家家解释的时间。最后再说一句真实感受接口加解密不是安全银弹不能替代权限管控、不能替代WAF、也不能替代全链路的日志审计。但它确实是把等保2.0在应用层传输数据安全这块抓得最牢的一层网。做技术选型时别上来就追最繁的方案先把RSAAES签名验签这套组合跑通再回头审视自己的业务场景是不是真的需要更复杂的国密体系或专用密码设备。能把这一整套逻辑自洽地讲清楚、落地好你面对测评和评审时就已经很有底气了。
返回列表