AES + RSA 混合加密方案
AES RSA 混合加密方案一、架构概览核心思想RSA 只用于密钥交换传输 AES 密钥AES-GCM 负责所有消息内容的加解密。非对称 对称混合兼顾安全与性能。二、算法参数参数值说明RSA 密钥长度2048 bit仅用于密钥交换RSA 填充模式OAEP-SHA256比 PKCS1v1.5 更安全抗选择密文攻击AES 密钥长度128 bit(16字节)GCM 模式每次加密随机生成 IVAES 模式AES/GCM/NoPadding自带认证标签防篡改GCM IV 长度12 字节(96 bit)每次加密随机生成GCM Tag 长度128 bit认证标签防篡改密文格式IV(12B) 密文 Tag(16B)拼接后统一 Base64 编码三、后端核心代码3.1 工具类 —CryptoUtils.javatalk-common/src/main/java/com/talk/common/utils/CryptoUtils.java// RSA 密钥对生成 publicstaticKeyPairgenerateRsaKeyPair(){returnSecureUtil.generateKeyPair(RSA,2048);}// RSA-OAEP 加密公钥加密 AES 密钥 privatestaticfinalStringRSA_ALGORITHMRSA/ECB/OAEPWithSHA-256AndMGF1Padding;publicstaticStringrsaEncrypt(Stringdata,StringpublicKey){PublicKeykeydecodePublicKey(publicKey);CiphercipherCipher.getInstance(RSA_ALGORITHM);OAEPParameterSpecspecnewOAEPParameterSpec(SHA-256,MGF1,MGF1ParameterSpec.SHA256,PSource.PSpecified.DEFAULT);cipher.init(Cipher.ENCRYPT_MODE,key,spec);byte[]encryptedcipher.doFinal(data.getBytes(StandardCharsets.UTF_8));returnBase64.encode(encrypted);}// AES-GCM 加密 publicstaticStringaesEncrypt(Stringplaintext,StringaesKey){byte[]keyBytesBase64.decode(aesKey);byte[]ivnewbyte[12];// 12字节随机 IVnewSecureRandom().nextBytes(iv);CiphercipherCipher.getInstance(AES/GCM/NoPadding);GCMParameterSpecspecnewGCMParameterSpec(128,iv);// 128-bit Tagcipher.init(Cipher.ENCRYPT_MODE,newSecretKeySpec(keyBytes,AES),spec);byte[]encryptedcipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));// IV 密文 拼接后 Base64byte[]resultnewbyte[iv.lengthencrypted.length];System.arraycopy(iv,0,result,0,iv.length);System.arraycopy(encrypted,0,result,iv.length,encrypted.length);returnBase64.encode(result);}3.2 密钥交换 —CryptoController.javatalk-chat/src/main/java/com/talk/chat/controller/CryptoController.java步骤 1获取公钥GetMapping(/public-key)publicAjaxResultKeyExchangeResponsegetPublicKey(){KeyPairkeyPairCryptoUtils.generateRsaKeyPair();StringpublicKeyBase64CryptoUtils.encodePublicKey(keyPair.getPublic());StringprivateKeyBase64CryptoUtils.encodePrivateKey(keyPair.getPrivate());// 完整 SHA-256 指纹防碰撞StringfingerprintDigestUtil.sha256Hex(publicKeyBase64);// 私钥存 Redis10分钟 TTLkey rsa_keypair:{fingerprint}redisTemplate.opsForValue().set(Constants.REDIS_RSA_KEY_PREFIXfingerprint,privateKeyBase64,10,TimeUnit.MINUTES);returnAjaxResult.success(newKeyExchangeResponse(publicKeyBase64,fingerprint));}步骤 2交换 AES 密钥PostMapping(/exchange)publicAjaxResultStringexchangeKey(ValidRequestBodyKeyExchangeRequestrequest){// 从 Redis 取私钥StringprivateKeyBase64redisTemplate.opsForValue().get(Constants.REDIS_RSA_KEY_PREFIXrequest.getFingerprint());// RSA 解密得到 AES 密钥StringaesKeyCryptoUtils.rsaDecrypt(request.getEncryptedAesKey(),privateKeyBase64);// AES 密钥存 Redis30分钟 TTLkey aes_key:{userId}:{keyId}StringkeyIdUUID.randomUUID().toString().replace(-,).substring(0,16);redisTemplate.opsForValue().set(Constants.REDIS_AES_KEY_PREFIXuserId:keyId,aesKey,30,TimeUnit.MINUTES);// 用完即弃删除 RSA 私钥redisTemplate.delete(Constants.REDIS_RSA_KEY_PREFIXrequest.getFingerprint());returnAjaxResult.success(keyId);}3.3 加密过滤器 —CryptoFilter.javatalk-framework/src/main/java/com/talk/framework/filter/CryptoFilter.java触发条件请求头X-Encrypted: true三层安全校验① 时间戳校验 → |now - timestamp| 5分钟防过期请求 ② Nonce 校验 → Redis 查重5分钟内不可重复防重放 ③ AES 密钥 → 从 Redis 取失效返回 401OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,...){// 1. 时间戳容差校验5分钟longtimestampLong.parseLong(request.getHeader(X-Timestamp));if(Math.abs(System.currentTimeMillis()-timestamp)5*60*1000){returnerror(请求已过期);}// 2. Nonce 防重放Redis 标记5分钟 TTLStringnonceKeynonce:request.getHeader(X-Nonce);if(redisTemplate.hasKey(nonceKey)){returnerror(请求重复);// 检测到重放攻击}redisTemplate.opsForValue().set(nonceKey,1,5,TimeUnit.MINUTES);// 3. 包装请求解密和响应加密CryptoRequestWrapperreqWrappernewCryptoRequestWrapper(request,aesKey);CryptoResponseWrapperresWrappernewCryptoResponseWrapper(response,aesKey);filterChain.doFilter(reqWrapper,resWrapper);resWrapper.finishResponse();// 加密响应体}跳过加密的路径shouldNotFilter/auth/、/crypto/、/upload/、/streamSSE、Swagger 文档等。四、前端核心代码4.1 加密工具 —crypto.jstalk-ui/src/api/crypto.js// AES-GCM 加密Web Crypto API浏览器原生 exportasyncfunctionaesEncrypt(plaintext,aesKeyBase64){constkeyBytesbase64ToArrayBuffer(aesKeyBase64)constkeyawaitcrypto.subtle.importKey(raw,keyBytes,{name:AES-GCM},false,[encrypt])constivcrypto.getRandomValues(newUint8Array(12))// 12字节随机 IVconstencodednewTextEncoder().encode(plaintext)constencryptedawaitcrypto.subtle.encrypt({name:AES-GCM,iv},key,encoded)// IV 密文 拼接constresultnewUint8Array(iv.lengthencrypted.byteLength)result.set(iv,0)result.set(newUint8Array(encrypted),iv.length)returnarrayBufferToBase64(result.buffer)}// RSA-OAEP 加密JSEncrypt 库exportfunctionrsaEncrypt(data,publicKeyPem){constencryptornewJSEncrypt()encryptor.setPublicKey(base64ToPem(publicKeyPem,PUBLIC))encryptor.setOptions({encryptionScheme:pkcs1_oaep,// OAEP 填充与后端一致signingScheme:pkcs1v15})returnencryptor.encrypt(data)}4.2 密钥交换流程exportasyncfunctionexchangeKeys(){// ① 获取服务端 RSA 公钥const{publicKey,fingerprint}awaitfetchPublicKey()// ② 本地生成 AES-128 密钥constaesKeyawaitgenerateAesKey()// ③ RSA-OAEP 加密 AES 密钥发送给服务端constencryptedAesKeyrsaEncrypt(aesKey,publicKey)constkeyIdawaitsendEncryptedAesKey(fingerprint,encryptedAesKey)// ④ 内存保存不持久化每次登录重新交换tokenManager.setCryptoKey(keyId,aesKey)}4.3 请求自动加解密 —request.js拦截talk-ui/src/api/request.js// 请求加密自动注入 X-Encrypted / X-Key-Id / X-Nonce / X-Timestampconst{encrypted,headers}awaitencryptRequest(requestData)// encrypted { keyId: xxx, data: Base64密文 }// 响应解密自动检测 EncryptedPayload 格式并解密constdecryptedawaitdecryptResponse(data)4.4 密钥生命周期 —token-manager.jstalk-ui/src/api/token-manager.js// 内存存储不持久化到 StoragesetCryptoKey(keyId,aesKey)// 登录后调用getCryptoKey()// 返回 { keyId, aesKey }clearCryptoKey()// 退出登录时清除// 退出登录时自动清除clearTokens(){cryptoKeyIdnull;// 密钥随登录态一起销毁cryptoAesKeynull;}五、Redis Key 设计Key 前缀格式TTL用途rsa_keypair:rsa_keypair:{fingerprint}10 分钟RSA 私钥交换阶段临时aes_key:aes_key:{userId}:{keyId}30 分钟AES 密钥与 Access Token 同生命周期nonce:nonce:{nonce}5 分钟防重放攻击标记六、安全措施总结措施实现机密性AES-GCM 加密消息内容密钥通过 RSA-OAEP 传输完整性GCM 模式自带 128-bit 认证标签篡改即解密失败防重放随机 nonce32位十六进制 Redis 去重 时间戳 5 分钟容差前向安全AES 密钥每次登录重新交换RSA 私钥用后即删密钥隔离AES 密钥按 userId 隔离存储key aes_key:{userId}:{keyId}最小暴露RSA 私钥仅存 Redis 10 分钟AES 密钥前端仅存内存降级兼容未交换密钥时自动降级为明文传输不影响基本功能SSE 豁免/stream路径跳过加密过滤器SSE 流不受影响七、涉及文件清单层文件职责后端-工具talk-common/.../utils/CryptoUtils.javaAES/RSA 加解密核心后端-DTOtalk-common/.../dto/crypto/EncryptedPayload.java加密载荷包装后端-DTOtalk-common/.../dto/crypto/KeyExchangeRequest.java密钥交换请求后端-DTOtalk-common/.../dto/crypto/KeyExchangeResponse.java密钥交换响应后端-常量talk-common/.../constant/Constants.javaRedis Key / Header / TTL后端-控制器talk-chat/.../controller/CryptoController.java密钥交换 API后端-过滤器talk-framework/.../filter/CryptoFilter.java请求解密 响应加密后端-包装器talk-framework/.../filter/CryptoRequestWrapper.java请求体解密后端-包装器talk-framework/.../filter/CryptoResponseWrapper.java响应体加密前端talk-ui/src/api/crypto.js前端 AES/RSA/密钥交换前端talk-ui/src/api/token-manager.js密钥内存管理前端talk-ui/src/api/request.js请求/响应自动加解密拦截前端talk-ui/src/pages/login/index.vue登录后触发密钥交换