1. 项目概述为什么API加解密是前后端分离的必修课最近在重构一个内部管理系统前端用的是Vue 3后端是SpringBoot。在联调接口时产品经理提了个需求部分涉及用户敏感信息的接口比如身份证号、手机号传输过程能不能“加个密”虽然我们用了HTTPS但他总觉得“再加一层更安心”。这个需求很合理也让我意识到在前后端分离架构成为主流的今天API层面的数据加解密早已从一个“可选项”变成了保障数据安全的“基础项”。这不仅仅是防止“中间人”窥探那么简单。想象一下你的应用日志里如果明文记录着完整的用户手机号运维同学排查问题时一眼就能看到这本身就是个隐患。或者前端需要将一些参数比如查询条件存储在LocalStorage里如果直接存明文一旦有XSS漏洞数据就暴露了。所以API加解密的核心价值在于在HTTPS提供的通道安全之外为业务数据本身再增加一层保护实现“端到端”的数据安全确保数据即使在传输过程中被截获、在日志中被记录、在前端被暂存攻击者也无法直接获取其原始内容。Vue和SpringBoot的组合一个负责视图交互和初步处理一个负责核心业务逻辑和最终校验是实践这套方案的黄金搭档。整个流程可以概括为前端Vue在发送请求前对特定业务参数进行加密将密文传输给后端后端SpringBoot接收到密文后先进行解密再执行业务逻辑返回数据时同理后端加密前端解密。听上去不复杂但里面涉及密钥管理、算法选型、异常处理、性能考量等一系列细节任何一个环节没处理好轻则功能异常重则引入新的安全漏洞。接下来我就结合这次实战把从设计到落地的完整过程以及踩过的坑详细拆解一遍。2. 核心方案设计与技术选型背后的思考接到需求第一反应不是马上写代码而是先定方案。加解密方案选型直接决定了后续开发的复杂度、系统的安全性和性能表现。我们需要回答几个关键问题用什么算法密钥怎么存哪些接口需要加解密2.1 对称加密 vs 非对称加密一个务实的选择这是首先要做的抉择。非对称加密如RSA安全性高公私钥分离但速度慢不适合加密大量数据。对称加密如AES速度快但密钥管理是个难题。对于绝大多数API交互场景我的建议是采用对称加密特别是AES算法。原因很直接性能API交互频繁响应速度是关键。AES加解密速度比RSA快几个数量级。数据量我们加密的通常是单个字段如手机号“13800138000”或一个小的JSON对象数据量很小完全在对称加密的高效处理范围内。场景匹配API加解密是双向的、可预见的通信前端和后端双方可以安全地共享同一个密钥。这与非对称加密主要解决“不可信双方安全通信”的场景有所不同。当然对称加密的密钥Secret Key需要前后端保持一致且保密。这引出了下一个关键问题密钥如何安全地存储在前端直接硬编码在Vue的代码里是绝对禁止的因为前端代码对用户是透明的。一个常见的实践是将密钥作为后端启动时的配置项如环境变量在用户登录成功后由后端通过HTTPS通道动态下发到前端。前端将其存储在内存或安全的存储区域如Vuex/Pinia store仅用于本次会话的加解密。这样即使刷新页面只要重新登录就能再次获取避免了密钥长期暴露在前端。2.2 算法模式与填充选择AES-256-GCM选定AES后还要确定具体的模式和填充方式。这里我强烈推荐AES-256-GCM。AES-256使用256位密钥强度足够应对当前及可预见的未来算力挑战。GCM模式全称Galois/Counter Mode。它不仅是加密模式还集成了消息认证功能。简单说它能在解密时验证数据是否被篡改同时还能生成一个“认证标签”。这比传统的CBC模式需要单独处理IV和填充更安全、更高效且能避免填充预言攻击Padding Oracle Attack。注意使用GCM模式时每次加密都需要一个唯一的随机数Nonce通常作为初始化向量IV。这个IV不需要保密但绝对不能重复使用相同的密钥和IV组合进行加密否则会严重破坏安全性。通常做法是每次加密随机生成一个IV并将其和密文一起传输给解密方。2.3 接口加解密的粒度与策略并非所有接口都需要不要一上来就给所有接口都加上加解密这会无谓地消耗性能增加复杂度。一个清晰的策略很重要全报文加密适用于对安全要求极高的场景如传输整个表单数据或核心业务报文。实现起来较复杂需要定义统一的请求/响应包装体。敏感字段加密更常见、更灵活的策略。只对如phone、idCard、email、password尽管密码本身应哈希处理等字段进行加密。其他字段如username、age等仍以明文传输。我选择了敏感字段加密。它在安全性和开发效率之间取得了很好的平衡。具体实现上需要前后端约定好哪些字段需要加密/解密通常可以通过字段名后缀如phoneEncrypted或一个独立的字段列表来标识。3. 后端SpringBoot实现详解构建稳健的解密防线后端是数据安全的最后一道关卡也是密钥的保管者实现必须稳健。我们分步骤来构建。3.1 环境准备与依赖引入首先创建一个SpringBoot项目这里以2.7.x版本为例在pom.xml中添加必要的依赖。我们主要需要两个库用于AES加解密的Bouncy Castle提供者以及用于处理JSON的JacksonSpringBoot已默认包含。dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15on/artifactId version1.70/version !-- 请使用最新稳定版 -- /dependencyBouncy Castle是一个强大的密码学库提供了JCEJava密码学扩展的更多实现对AES-GCM的支持更完善。3.2 核心加解密工具类设计我将加解密逻辑封装在一个AesGcmUtil工具类中。这里有几个关键设计点密钥管理密钥从application.yml配置文件中读取通过Value注入。在生产环境中这个配置值应该来自环境变量或配置中心绝不能写入代码仓库。异常处理加解密可能失败如密文被篡改、IV错误必须抛出明确的、受检的异常如自定义的CryptoException便于全局异常处理器统一处理返回给前端友好的错误信息而不是抛出堆栈信息。线程安全工具类设计为无状态静态方法或通过Spring管理的Bean确保线程安全。以下是工具类的核心代码片段import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.security.Security; import java.util.Base64; public class AesGcmUtil { private static final String ALGORITHM AES/GCM/NoPadding; private static final int TAG_LENGTH_BIT 128; // GCM认证标签长度128位是标准且安全的 private static final int IV_LENGTH_BYTE 12; // 推荐使用12字节的IV兼容性好且安全 static { // 注册BouncyCastle提供者 Security.addProvider(new BouncyCastleProvider()); } /** * 加密 * param plaintext 明文 * param secretKey 密钥Base64编码字符串 * return Base64编码的字符串格式为IV 密文 */ public static String encrypt(String plaintext, String secretKey) throws Exception { byte[] keyBytes Base64.getDecoder().decode(secretKey); SecretKey key new SecretKeySpec(keyBytes, AES); // 1. 生成随机IV byte[] iv new byte[IV_LENGTH_BYTE]; SecureRandom random new SecureRandom(); random.nextBytes(iv); // 2. 初始化Cipher为加密模式 Cipher cipher Cipher.getInstance(ALGORITHM, BC); GCMParameterSpec parameterSpec new GCMParameterSpec(TAG_LENGTH_BIT, iv); cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); // 3. 执行加密 byte[] ciphertextBytes cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); // 4. 将IV和密文拼接然后整体Base64编码 byte[] combined new byte[iv.length ciphertextBytes.length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(ciphertextBytes, 0, combined, iv.length, ciphertextBytes.length); return Base64.getEncoder().encodeToString(combined); } /** * 解密 * param ciphertextWithIv Base64编码的字符串IV密文 * param secretKey 密钥Base64编码字符串 * return 明文 */ public static String decrypt(String ciphertextWithIv, String secretKey) throws Exception { byte[] keyBytes Base64.getDecoder().decode(secretKey); SecretKey key new SecretKeySpec(keyBytes, AES); // 1. Base64解码分离IV和密文 byte[] combined Base64.getDecoder().decode(ciphertextWithIv); byte[] iv new byte[IV_LENGTH_BYTE]; byte[] ciphertextBytes new byte[combined.length - IV_LENGTH_BYTE]; System.arraycopy(combined, 0, iv, 0, IV_LENGTH_BYTE); System.arraycopy(combined, IV_LENGTH_BYTE, ciphertextBytes, 0, ciphertextBytes.length); // 2. 初始化Cipher为解密模式 Cipher cipher Cipher.getInstance(ALGORITHM, BC); GCMParameterSpec parameterSpec new GCMParameterSpec(TAG_LENGTH_BIT, iv); cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); // 3. 执行解密 byte[] plaintextBytes cipher.doFinal(ciphertextBytes); return new String(plaintextBytes, StandardCharsets.UTF_8); } /** * 生成一个随机的AES-256密钥Base64编码 */ public static String generateRandomSecretKey() throws Exception { SecureRandom random new SecureRandom(); byte[] keyBytes new byte[32]; // 256位 32字节 random.nextBytes(keyBytes); return Base64.getEncoder().encodeToString(keyBytes); } }实操心得IV初始化向量必须是随机且唯一的。这里我使用SecureRandom来生成它是密码学安全的随机数生成器比普通的Random类安全得多。将IV和密文拼接后一起传输是通用做法解密方需要知道这个IV才能正确解密。3.3 集成到SpringBoot过滤器、拦截器还是注解如何让加解密逻辑优雅地切入到SpringMVC的处理流程中有三种常见方式过滤器Filter最早接触到请求可以处理HttpServletRequest和HttpServletResponse的输入输出流。但在这里操作流来解密请求体、加密响应体会比较繁琐且容易破坏Spring的序列化/反序列化机制。拦截器Interceptor在控制器方法执行前后介入可以拿到处理后的参数和返回值。更适合我们的场景因为我们可以针对RequestBody的对象进行解密对返回的对象进行加密。自定义注解AOP最灵活、侵入性最低的方式。可以精确控制哪些接口、哪些参数需要加解密。我选择了自定义注解AOP的方式因为它最符合“声明式”编程的理念代码清晰耦合度低。首先定义两个注解// 用于标记需要解密的方法参数通常是RequestBody对象 Target(ElementType.PARAMETER) Retention(RetentionPolicy.RUNTIME) public interface DecryptParam { } // 用于标记需要加密返回值的方法 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface EncryptResponse { }然后编写一个切面Aspect来处理这些注解Component Aspect Slf4j public class CryptoAspect { Value(${api.crypto.secret-key}) private String secretKey; /** * 环绕通知处理EncryptResponse注解 */ Around(annotation(encryptResponse)) public Object aroundEncrypt(ProceedingJoinPoint joinPoint, EncryptResponse encryptResponse) throws Throwable { // 1. 执行原方法获取返回值 Object result joinPoint.proceed(); if (result null) { return null; } // 2. 这里假设返回值是一个简单的包装类里面有一个需要加密的字段 // 实际项目中你可能需要递归遍历对象加密指定字段 if (result instanceof ApiResponse) { ApiResponse apiResponse (ApiResponse) result; Object data apiResponse.getData(); if (data ! null data instanceof SensitiveData) { SensitiveData sensitiveData (SensitiveData) data; String encryptedPhone AesGcmUtil.encrypt(sensitiveData.getPhone(), secretKey); sensitiveData.setPhoneEncrypted(encryptedPhone); // 可以选择清空明文或者保留根据业务定 // sensitiveData.setPhone(null); } } // 更通用的做法是定义一个接口如SensitiveObject里面有encryptFields()和decryptFields()方法在切面中调用。 return result; } /** * 前置通知处理DecryptParam注解 */ Before(annotation(decryptParam)) public void beforeDecrypt(JoinPoint joinPoint, DecryptParam decryptParam) { Object[] args joinPoint.getArgs(); for (Object arg : args) { if (arg ! null arg instanceof SensitiveRequest) { SensitiveRequest request (SensitiveRequest) arg; try { String decryptedPhone AesGcmUtil.decrypt(request.getPhoneEncrypted(), secretKey); request.setPhone(decryptedPhone); } catch (Exception e) { log.error(参数解密失败, e); throw new CryptoException(请求参数解密错误); } } } } }最后在控制器中我们就可以非常清晰地使用RestController RequestMapping(/user) public class UserController { PostMapping(/update) EncryptResponse // 标记此方法返回值需要加密处理 public ApiResponse updateUser(RequestBody DecryptParam SensitiveRequest request) { // 此时request.getPhone()已经是解密后的明文 userService.updatePhone(request.getPhone()); return ApiResponse.success(更新成功); } }踩坑记录最初我尝试在过滤器中直接读取ServletRequest的输入流进行解密然后替换为一个新的ContentCachingRequestWrapper。这导致了Spring的RequestBody注解无法正确绑定参数因为流已经被读取过一次了。最终切换到AOP方案在参数绑定之后、方法执行之前介入完美解决了问题。这告诉我在Spring生态里选择与框架生命周期匹配的扩展点至关重要。4. 前端Vue 3 Axios实现详解构建安全的请求链路前端是数据的发起方也是最终展示方它的任务是安全地加密数据、解密数据并且对用户透明。4.1 封装通用的加解密函数首先在Vue项目中创建一个utils/crypto.js文件使用crypto-js库来实现AES-GCM加解密。crypto-js是一个广泛使用的JavaScript加密库功能全面。npm install crypto-js然后编写工具函数import CryptoJS from crypto-js; // 注意crypto-js的AES默认使用CBC模式和PKCS7填充。 // 为了与后端Java的AES/GCM/NoPadding兼容我们需要做一些额外处理。 // 实际上crypto-js对GCM模式的支持不如Node.js原生crypto模块。 // 更推荐在Vue浏览器环境中使用Web Crypto API它是现代浏览器原生支持的标准。 // 这里展示使用Web Crypto API的实现更安全、更标准 class AesGcmUtil { constructor(secretKeyBase64) { // 将Base64格式的密钥字符串转换为CryptoKey对象 this.secretKeyPromise this.importSecretKey(secretKeyBase64); } async importSecretKey(secretKeyBase64) { const keyBytes Uint8Array.from(atob(secretKeyBase64), c c.charCodeAt(0)); return await window.crypto.subtle.importKey( raw, keyBytes, { name: AES-GCM }, false, // 是否可导出 [encrypt, decrypt] ); } async encrypt(plaintext) { const key await this.secretKeyPromise; const iv window.crypto.getRandomValues(new Uint8Array(12)); // 12字节IV const encodedPlaintext new TextEncoder().encode(plaintext); const ciphertext await window.crypto.subtle.encrypt( { name: AES-GCM, iv: iv }, key, encodedPlaintext ); // 将IV和密文拼接然后转为Base64 const combined new Uint8Array(iv.length ciphertext.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(ciphertext), iv.length); return btoa(String.fromCharCode(...combined)); } async decrypt(ciphertextWithIvBase64) { const key await this.secretKeyPromise; const combined Uint8Array.from(atob(ciphertextWithIvBase64), c c.charCodeAt(0)); const iv combined.slice(0, 12); const ciphertext combined.slice(12); const decryptedBuffer await window.crypto.subtle.decrypt( { name: AES-GCM, iv: iv }, key, ciphertext ); return new TextDecoder().decode(decryptedBuffer); } } // 导出单例或工厂函数 let cryptoInstance null; export function getCryptoInstance(secretKey) { if (!cryptoInstance) { cryptoInstance new AesGcmUtil(secretKey); } return cryptoInstance; }重要提示Web Crypto API是异步的返回Promise。这意味着我们的加解密函数也必须是异步的。在实际封装时需要考虑如何优雅地处理异步调用避免在业务代码中到处写async/await。4.2 集成Axios拦截器自动化的请求加密与响应解密为了让业务开发人员无感知地使用加解密功能最佳实践是将其集成到Axios的请求/响应拦截器中。首先在src/utils/request.js或你的Axios实例文件中import axios from axios; import { getCryptoInstance } from ./crypto; import { useUserStore } from /stores/user; // 假设使用Pinia存储密钥 // 创建axios实例 const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }); // 请求拦截器 service.interceptors.request.use( async (config) { const userStore useUserStore(); const crypto getCryptoInstance(userStore.secretKey); // 从状态管理获取密钥 // 判断是否需要加密请求数据 if (config.data config.__encryptRequest) { // 通过自定义属性标记 // 假设我们需要加密data中的phone字段 if (config.data.phone) { try { config.data.phoneEncrypted await crypto.encrypt(config.data.phone); delete config.data.phone; // 删除明文只传密文 } catch (error) { console.error(请求参数加密失败:, error); return Promise.reject(error); } } } return config; }, (error) { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( async (response) { const userStore useUserStore(); const crypto getCryptoInstance(userStore.secretKey); // 判断是否需要解密响应数据 if (response.data response.config.__decryptResponse) { // 假设后端返回的数据结构为 { code: 200, data: { phoneEncrypted: ... }, message: ok } const data response.data.data; if (data data.phoneEncrypted) { try { data.phone await crypto.decrypt(data.phoneEncrypted); delete data.phoneEncrypted; // 解密后移除密文字段 } catch (error) { console.error(响应数据解密失败:, error); // 可以选择抛出错误或保持密文 } } } return response; }, (error) { return Promise.reject(error); } ); export default service;然后我们可以在发起请求时通过自定义配置项来标记这个请求需要加解密处理// 在Vue组件中 import request from /utils/request; export default { methods: { async updateUserInfo(phone) { try { const response await request({ url: /user/update, method: post, data: { phone }, __encryptRequest: true, // 自定义标记请求加密 __decryptResponse: true // 自定义标记响应解密 }); console.log(更新成功解密后的手机号:, response.data.data.phone); } catch (error) { console.error(更新失败, error); } } } }实操心得直接在config上添加__encryptRequest这样的自定义属性虽然方便但会污染Axios的配置对象。一个更规范的做法是定义自己的请求函数或者利用Axios的transformRequest和transformResponse配置项。但拦截器方案在逻辑集中管理上更清晰。关键在于加解密逻辑应该对业务组件透明业务开发人员只需要关心“这个接口需要加密”而不需要知道“怎么加密”。4.3 密钥的安全获取与存储密钥不能写死在前端代码里。一个可行的流程是用户登录成功后后端除了返回Token还可以返回一个本次会话有效的加密密钥可以是对主密钥进行对称加密后的结果或者由后端动态生成。前端Vue将这个密钥存储在内存中如Pinia/Vuex store不要存到LocalStorage或Cookie中以防XSS攻击窃取。刷新页面后密钥丢失需要重新登录获取。对于单页应用(SPA)可以考虑在应用初始化时通过一个携带Token的静默请求向后端重新获取密钥。// 在登录成功的处理逻辑中 login().then(async (res) { const token res.data.token; const secretKey res.data.secretKey; // 后端返回的加密密钥 // 存储到Pinia const userStore useUserStore(); userStore.setToken(token); userStore.setSecretKey(secretKey); // 初始化加密工具实例 getCryptoInstance(secretKey); });5. 联调、测试与上线前必须验证的环节前后端代码都写好了但直接上线就是赌博。必须经过严格的测试。5.1 单元测试确保工具类可靠后端的AesGcmUtil和前端对应的加解密函数必须要有单元测试覆盖正常加解密、错误密文、错误密钥、空数据等场景。Java (JUnit 5) 示例SpringBootTest class AesGcmUtilTest { String secretKey; BeforeEach void setUp() throws Exception { secretKey AesGcmUtil.generateRandomSecretKey(); } Test void testEncryptAndDecrypt() throws Exception { String originalText 13800138000; String encrypted AesGcmUtil.encrypt(originalText, secretKey); assertNotNull(encrypted); assertNotEquals(originalText, encrypted); String decrypted AesGcmUtil.decrypt(encrypted, secretKey); assertEquals(originalText, decrypted); } Test void testDecryptWithWrongKey() { String originalText test; String encrypted AesGcmUtil.encrypt(originalText, secretKey); String wrongKey AesGcmUtil.generateRandomSecretKey(); // 另一个随机密钥 Exception exception assertThrows(Exception.class, () - { AesGcmUtil.decrypt(encrypted, wrongKey); }); // 应该抛出如BadPaddingException, AEADBadTagException等 } }JavaScript (Jest/Vitest) 示例import { getCryptoInstance } from ./crypto; describe(AES-GCM Crypto, () { const secretKey your-base64-secret-key-for-test; // 测试用固定密钥 let crypto; beforeAll(async () { crypto getCryptoInstance(secretKey); }); test(should encrypt and decrypt text correctly, async () { const plaintext 13800138000; const ciphertext await crypto.encrypt(plaintext); expect(ciphertext).toBeTruthy(); expect(ciphertext).not.toBe(plaintext); const decrypted await crypto.decrypt(ciphertext); expect(decrypted).toBe(plaintext); }); test(should throw error when decrypting with tampered data, async () { const plaintext test; const ciphertext await crypto.encrypt(plaintext); // 模拟密文被篡改修改Base64字符串的一个字符 const tamperedCiphertext ciphertext.slice(0, -5) xxxxx; await expect(crypto.decrypt(tamperedCiphertext)).rejects.toThrow(); }); });5.2 集成测试模拟真实请求使用Postman或编写集成测试脚本模拟前端发送加密请求验证后端能否正确解密并处理以及返回的加密响应前端能否正确解密。Postman测试脚本示例 (Pre-request Script):// 使用 crypto-js 库需要在Postman的Tests标签页中引入 // 或者使用更简单的 nodejs crypto 模块如果Postman支持 const CryptoJS require(crypto-js); const secretKey pm.environment.get(SECRET_KEY_BASE64); const plainPhone 13800138000; // 简单模拟加密实际应与前端算法一致 function encrypt(plaintext, key) { // 这里仅为示例实际应使用Web Crypto API或与后端一致的算法 // 假设使用AES-ECB仅用于演示生产环境勿用ECB const encrypted CryptoJS.AES.encrypt(plaintext, CryptoJS.enc.Base64.parse(key)); return encrypted.toString(); } pm.environment.set(ENCRYPTED_PHONE, encrypt(plainPhone, secretKey));然后在请求Body中使用{{ENCRYPTED_PHONE}}变量。5.3 性能与兼容性测试性能使用JMeter或LoadRunner对加解密的接口进行压力测试观察引入加解密后接口平均响应时间RT和吞吐量QPS的变化。在我的测试中对于单字段的AES-256-GCM加解密RT增加通常在1-5毫秒内对于大多数应用是可接受的。但如果加密整个大的JSON报文则需要评估影响。兼容性确保后端使用的加密算法如AES/GCM/NoPadding在所有目标部署环境JDK版本中都可用。前端Web Crypto API的AES-GCM支持情况良好但如果你需要支持非常老的浏览器如IE11则需要使用crypto-js等polyfill并注意算法对齐。6. 常见问题排查与进阶优化方案在实际开发和运维中你肯定会遇到各种问题。这里记录几个典型问题及其解决方案。6.1 加解密失败从异常信息快速定位异常现象 (后端Java)可能原因排查步骤javax.crypto.AEADBadTagException1. 密钥不匹配。2. 密文被篡改GCM认证失败。3. IV不匹配或重复使用。1. 确认前后端密钥完全一致Base64字符串比对。2. 检查网络传输中密文是否被截获修改。可对比前端发送和后端接收的Base64字符串。3. 确保每次加密使用随机IV且解密时IV提取正确。javax.crypto.BadPaddingException1. 使用了错误的算法或模式如后端GCM前端CBC。2. 密钥长度不对如用了128位的密钥但配置成256位。1. 前后端严格统一算法字符串如AES/GCM/NoPadding。2. 检查密钥生成和配置AES-256需要32字节的密钥。解密得到乱码1. 字符编码不一致。2. IV和密文拼接/分离的逻辑不一致。1. 前后端统一使用UTF-8编码。2. 调试打印出前端加密后的完整Base64串和后端接收到的串逐字符比对。检查IV长度12字节和拼接顺序。前端排查要点在浏览器开发者工具的“网络”选项卡中查看发送的请求负载Payload确认加密字段如phoneEncrypted的值是否是一个合法的、较长的Base64字符串。使用console.log在加密函数前后打印明文和密文确认加密过程无误。确保用于解密的密钥和加密时的密钥是同一个。6.2 密钥轮转与安全增强一个密钥永远不换是不安全的。需要设计密钥轮转策略。方案一简单定期如每月在后端更换密钥并重启服务。前端在下次登录时获取新密钥。缺点是服务重启有间隔期且已登录用户会解密失败。方案二优雅支持多版本密钥。后端同时维护当前密钥和上一个版本的密钥。解密时先用当前密钥尝试失败则用旧密钥尝试。加密始终用最新密钥。前端在收到“密钥过期”的错误码时主动调用一个特定接口获取新密钥。这需要前后端约定好密钥版本管理协议。6.3 监控与日志脱敏引入加解密后监控和日志需要特别处理监控对加解密失败的错误进行监控和告警。突然大量的CryptoException可能意味着密钥泄露或攻击。日志脱敏确保日志框架如Logback、Log4j2不会打印出完整的加密请求/响应体。可以配置日志模式将包含Encrypted字样的字段值替换为[ENCRYPTED]。千万不能把密文甚至密钥打印到日志里!-- logback.xml 示例 -- configuration conversionRule conversionWordmask converterClasscom.yourpackage.MaskingPatternLayout / appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mask(%msg) %n/pattern /encoder /appender ... /configuration6.4 应对更复杂的场景非对称加密混合方案如果安全性要求极高可以考虑混合加密使用RSA来加密传输AES的会话密钥。后端生成一对RSA公私钥公钥下发给前端。前端每次会话或每次重要请求随机生成一个AES会话密钥session key。前端用后端的RSA公钥加密这个AES会话密钥将其和用该会话密钥加密的业务数据一起发送给后端。后端用RSA私钥解密出AES会话密钥再用它解密业务数据。这种方式结合了非对称加密的安全性和对称加密的效率但复杂度也大大增加通常用于金融、支付等对安全极其敏感的领域。对于大多数内部管理系统或一般Web应用使用HTTPS 固定的AES密钥对敏感字段加密已经能提供足够的安全保障。整个项目做下来最大的体会是安全是一个系统性问题而不是一个功能点。API加解密只是其中一环它需要前后端紧密协作、统一约定并且与密钥管理、异常处理、监控日志等周边设施联动才能形成一个有效的防御体系。不能只追求“实现了加密”更要关注“是否安全地实现了加密”。每次加密操作都伴随着性能开销和复杂度提升所以一定要有清晰的边界只保护真正需要保护的数据这样才能在安全和效率之间找到最佳的平衡点。