客户端与服务端数据加密方案及实现
在前后端交互中,以下业务场景通常需要进行数据加密:
需要加密的典型业务场景
- 用户认证信息:登录用户名、密码、短信验证码
- 个人敏感信息:身份证号、银行卡号、手机号
- 财务数据:交易金额、账户余额、支付信息
- 隐私内容:私信、医疗记录、位置信息
- 业务敏感数据:合同内容、商业机密、专利信息
推荐加密方案
方案1:HTTPS + 敏感字段单独加密(推荐)
- 所有通信走HTTPS
- 对特别敏感的数据在应用层额外加密
方案2:HTTPS + 全报文加密(高安全需求)
- 所有通信走HTTPS
- 整个请求/响应体进行加密
完整实现示例
前端实现 (Vue3/JavaScript)
安装加密库
npm install crypto-js
加密工具类 (utils/crypto.js)
import CryptoJS from 'crypto-js'// AES加密 (CBC模式)
export function encryptAES(data, key, iv) {const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data),CryptoJS.enc.Utf8.parse(key),{iv: CryptoJS.enc.Utf8.parse(iv),mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7})return encrypted.toString()
}// AES解密
export function decryptAES(ciphertext, key, iv) {const decrypted = CryptoJS.AES.decrypt(ciphertext,CryptoJS.enc.Utf8.parse(key),{iv: CryptoJS.enc.Utf8.parse(iv),mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7})return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
}// 生成随机密钥和IV (前端生成,通过RSA公钥加密传输给后端)
export function generateRandomKey() {const key = CryptoJS.lib.WordArray.random(32).toString()const iv = CryptoJS.lib.WordArray.random(16).toString()return { key, iv }
}
API请求封装示例
import { encryptAES, generateRandomKey } from '@/utils/crypto'// 假设这是从后端获取的RSA公钥
const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6qHfR6pJZQ...`// 加密请求数据
async function encryptRequestData(data) {// 生成临时AES密钥const { key, iv } = generateRandomKey()// 用AES加密实际数据const encryptedData = encryptAES(data, key, iv)// 用RSA加密AES密钥const encryptedKey = await window.crypto.subtle.encrypt({ name: "RSA-OAEP" },await importRsaKey(RSA_PUBLIC_KEY),new TextEncoder().encode(key))return {data: encryptedData,key: arrayBufferToBase64(encryptedKey),iv: iv}
}// 发送加密请求
async function sendEncryptedRequest(url, payload) {const encrypted = await encryptRequestData(payload)return axios.post(url, encrypted)
}// 示例:登录请求
async function login(username, password) {const response = await sendEncryptedRequest('/api/login', {username,password})return response.data
}
后端实现 (Java Spring Boot)
添加依赖 (pom.xml)
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.70</version>
</dependency>
加密工具类 (AesUtils.java)
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;public class AesUtils {private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";public static String encrypt(String data, String key, String iv) throws Exception {SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));Cipher cipher = Cipher.getInstance(AES_ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encryptedData);}public static String decrypt(String encryptedData, String key, String iv) throws Exception {SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));Cipher cipher = Cipher.getInstance(AES_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);byte[] decodedData = Base64.getDecoder().decode(encryptedData);byte[] decryptedData = cipher.doFinal(decodedData);return new String(decryptedData, StandardCharsets.UTF_8);}
}
RSA工具类 (RsaUtils.java)
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;public class RsaUtils {private static final String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";// 解密AES密钥public static String decryptAesKey(String encryptedKey, String privateKeyStr) throws Exception {byte[] encryptedBytes = Base64.getDecoder().decode(encryptedKey);PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr));PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, privateKey);byte[] decryptedBytes = cipher.doFinal(encryptedBytes);return new String(decryptedBytes, StandardCharsets.UTF_8);}
}
控制器示例 (LoginController.java)
@RestController
@RequestMapping("/api")
public class LoginController {@Value("${rsa.private-key}")private String rsaPrivateKey;@PostMapping("/login")public ResponseEntity<?> login(@RequestBody EncryptedRequest request) {try {// 1. 用RSA私钥解密AES密钥String aesKey = RsaUtils.decryptAesKey(request.getKey(), rsaPrivateKey);// 2. 用AES密钥解密数据String decryptedData = AesUtils.decrypt(request.getData(), aesKey, request.getIv());// 3. 处理业务逻辑LoginDTO loginDTO = objectMapper.readValue(decryptedData, LoginDTO.class);// ... 验证用户名密码等业务逻辑// 4. 加密响应数据String responseData = AesUtils.encrypt(objectMapper.writeValueAsString(response),aesKey,request.getIv());return ResponseEntity.ok(new EncryptedResponse(responseData));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}// 请求体public static class EncryptedRequest {private String data; // AES加密的业务数据private String key; // RSA加密的AES密钥private String iv; // AES IV// getters & setters}// 响应体public static class EncryptedResponse {private String data; // AES加密的响应数据// constructor & getter}
}
密钥管理最佳实践
-
前端:
- 每次会话生成新的AES密钥
- 使用RSA公钥加密传输AES密钥
- 不要在前端存储长期有效的密钥
-
后端:
- 使用安全的密钥管理系统存储RSA私钥
- 定期轮换RSA密钥对
- 对不同业务使用不同的密钥
-
传输层:
- 必须使用HTTPS
- 启用HSTS防止SSL剥离攻击
以下是加密解密流程及密钥来源说明:
密钥来源说明(表格版)
组件 | 密钥类型 | 来源说明 | 存储位置 |
---|---|---|---|
RSA公钥 | 非对称公钥 | 1. 服务端生成密钥对 2. 公钥预置在客户端或首次连接时动态获取 | 客户端配置文件/内存 |
RSA私钥 | 非对称私钥 | 服务端生成后存储在安全位置(HSM/KMS/配置文件) | 服务端安全存储 |
AES密钥 | 对称密钥 | 1. 客户端每次会话随机生成 2. 通过RSA加密传输给服务端 | 会话期间内存存储 |
IV向量 | 初始化向量 | 客户端随机生成,随请求一起传输 | 不存储,每次请求重新生成 |
关键流程图示
-
密钥交换流程:
[客户端] [服务端]|-- RSA公钥 (预置) ------>|| ||-- RSA加密的AES密钥 ---->|| |-- RSA私钥解密获得AES密钥
-
数据加密流程:
[客户端业务数据] --> AES加密(key+iv) --> 密文数据--> 通过HTTPS传输--> 服务端AES解密--> 明文业务数据
-
密钥生命周期:
AES密钥生成 -> RSA加密传输 -> 内存暂存 -> 会话结束销毁 (客户端) (HTTPS通道) (服务端) (自动清除)
这种设计结合了:
- RSA的非对称加密安全性(用于密钥交换)
- AES的对称加密高效性(用于数据加密)
- 每次会话独立的密钥(前向安全性)
- HTTPS的传输层保护(防窃听)
性能考虑
-
对于性能敏感场景,可以:
- 会话期间缓存AES密钥
- 对非敏感数据不加密或使用更轻量级的加密方式
- 考虑使用ChaCha20-Poly1305替代AES-GCM(在移动设备上性能更好)
-
对于高安全需求场景:
- 实现完整的端到端加密
- 考虑使用专业的加密库如Google Tink