密码与加密基础篇(5):AES 和 RSA 到底怎么配合?一次讲清 App 接口混合加密
前面几篇
密码与加密基础篇(1):别再说 MD5 加密了:编码、摘要、加密到底有什么区别?-CSDN博客
密码与加密基础篇(2):密码到底怎么存?为什么 MD5 已经过时?-CSDN博客
密码与加密基础篇(3):salt 和 pepper 到底是什么?为什么 salt 可以放数据库?-CSDN博客
密码与加密基础篇(4):bcrypt / Argon2id 为什么比 MD5 更适合存密码?-CSDN博客
我们一直在讲密码存储:
第1篇:别再说 MD5 加密了:编码、摘要、加密到底有什么区别? 第2篇:密码到底怎么存?为什么 MD5 已经过时? 第3篇:salt 和 pepper 到底是什么?为什么 salt 可以放数据库? 第4篇:bcrypt / Argon2id 为什么比 MD5 更适合存密码?这几篇主要解决的是:
密码怎么存,为什么不能明文存,为什么不能简单 MD5,为什么要用 bcrypt / Argon2id。
这一篇开始进入真正的“加密传输 / 业务层接口加密”。
很多开发一听到接口加密,就会想到:
AES RSA HTTPS 公钥 私钥 密文 签名但真正落到 App 和后端接口设计里,经常会卡在一个问题上:
AES 和 RSA 到底怎么配合?
这篇文章就讲清楚这个问题。
一句话先说结论:
AES 负责加密真正的业务数据,RSA 负责加密 AES key。
或者再短一点:
AES 加密数据,RSA 加密 AES 密钥。
一、先区分:AES 和 RSA 分别是什么?
AES 和 RSA 都和加密有关,但它们不是一类东西。
AES 是对称加密。
RSA 是非对称加密。
二、AES:对称加密,适合加密数据
AES 的特点是:
同一个 key 加密 同一个 key 解密流程是:
明文数据 ↓ AES key 加密 密文数据 ↓ AES key 解密 明文数据所以 AES 叫对称加密。
比如 App 要加密一个登录请求:
{ "username": "wu", "password": "123456" }可以用 AES-GCM 加密成:
cipherText = AES-GCM(username/password JSON, aesKey, iv)AES 的优点是:
速度快 适合加密大量数据 适合加密 JSON 请求体 适合加密文件 适合本地 Token 加密存储但 AES 有一个核心问题:
AES key 怎么安全地给后端?
因为 AES 加密和解密用的是同一把 key。
如果 App 和后端都要解密请求体,那么 App 和后端都要知道这把 AES key。
问题来了:
AES key 写死在 App 里?不行。
因为 APK 可以反编译。
AES key 明文传给后端?也不行。
因为传 key 本身就暴露了。
所以 AES 很适合加密数据,但它自己解决不了“密钥安全传递”的问题。
三、RSA:非对称加密,适合保护密钥
RSA 和 AES 不一样。
RSA 有一对 key:
公钥 public key 私钥 private key它的典型用法有两种:
公钥加密,私钥解密 私钥签名,公钥验签如果用于接口加密,常见方式是:
后端保存 RSA 私钥 App 内置或动态获取 RSA 公钥App 用后端公钥加密数据。
后端用自己的私钥解密数据。
因为私钥只在后端,App 不需要知道私钥。
这就解决了一个问题:
公钥可以给客户端 私钥只在服务端但 RSA 也有明显缺点:
速度慢 不适合加密大量数据 可加密的数据长度有限所以 RSA 不适合直接加密整个请求体。
比如你有一个很大的 JSON,甚至文件上传、图片、设备数据、日志数据,都不适合直接用 RSA 全量加密。
因此,RSA 更适合加密少量数据。
最典型的少量数据就是:
AES key四、为什么不能只用 AES?
只用 AES 最大的问题是:
AES key 怎么安全给后端?错误做法一:
private const val AES_KEY = "1234567890123456"这样把 AES key 写死在客户端,风险很大。
因为 APK 可以被反编译。
一旦 key 被拿到,所有使用这把 key 加密的数据都有风险。
错误做法二:
{ "aesKey": "1234567890123456", "cipherText": "xxxx" }这等于把钥匙和保险箱一起寄出去了。
没有意义。
所以只用 AES 不够。
AES 需要一个安全传递密钥的方案。
五、为什么不能只用 RSA?
只用 RSA 的问题是:
RSA 不适合加密大量数据。比如登录请求体还比较小:
{ "username": "wu", "password": "123456" }看起来好像可以直接 RSA 加密。
但真实业务接口可能是:
{ "deviceId": "xxx", "location": { "lat": 31.2, "lng": 120.6 }, "data": [ ... ], "extra": { ... } }请求体可能很大。
RSA 加密大数据性能差,而且有长度限制。
所以工程里不会用 RSA 加密所有业务数据。
RSA 更适合做:
加密 AES key 签名 验签 密钥交换六、AES + RSA 的经典配合
AES 和 RSA 的经典配合就是:
AES 加密业务数据 RSA 加密 AES key也就是混合加密。
流程如下:
App 生成一个随机 AES key ↓ App 用 AES-GCM 加密业务数据 ↓ App 用后端 RSA 公钥加密 AES key ↓ App 发送 encryptedKey + iv + cipherText 给后端 ↓ 后端用 RSA 私钥解密 encryptedKey,得到 AES key ↓ 后端用 AES key + iv 解密 cipherText,得到业务明文这就是 AES 和 RSA 最经典的配合方式。
它结合了两者优点:
AES: 加密业务数据,速度快。 RSA: 加密 AES key,解决密钥传递问题。一句话:
RSA 不直接加密大数据,而是保护 AES key;真正的数据加密交给 AES。
七、一次 App 登录请求怎么做混合加密?
假设登录请求原文是:
{ "username": "wu", "password": "123456" }如果做业务层 AES + RSA 混合加密,App 端可以这样处理:
1. 把登录参数转成 JSON 字符串 2. 随机生成一个临时 AES key 3. 随机生成一个 IV 4. 使用 AES-GCM 加密登录 JSON,得到 cipherText 5. 使用后端 RSA 公钥加密 AES key,得到 encryptedKey 6. 把 encryptedKey、iv、cipherText 发给后端最终请求体可能变成:
{ "encryptedKey": "RSA公钥加密后的AES key", "iv": "AES-GCM使用的IV", "cipherText": "AES-GCM加密后的登录请求体", "timestamp": 1780000000000, "nonce": "random-string" }这时网络上传输的就不是:
{ "username": "wu", "password": "123456" }而是密文。
八、后端怎么解密?
后端收到请求后:
1. 读取 encryptedKey 2. 使用 RSA 私钥解密 encryptedKey,得到 AES key 3. 读取 iv 和 cipherText 4. 使用 AES key + iv 解密 cipherText 5. 得到原始 JSON 6. 继续走登录逻辑解密后,后端拿到:
{ "username": "wu", "password": "123456" }然后后端再做密码验证:
用户输入密码 ↓ bcrypt / Argon2id / PBKDF2 校验 ↓ 登录成功 ↓ 签发 accessToken / refreshToken注意,这里不要混。
AES + RSA 解决的是:
请求体在业务层加密传输bcrypt / Argon2id 解决的是:
密码在数据库中的安全存储和验证它们不是同一层问题。
九、encryptedKey、iv、cipherText 分别是什么?
一次混合加密请求里,常见字段有三个。
1. encryptedKey
encryptedKey = RSA公钥加密后的AES key它的作用是:
把本次请求使用的 AES key 安全交给后端。App 生成 AES key 后,不能明文发给后端。
所以要用后端 RSA 公钥加密。
后端用 RSA 私钥才能解开。
2. iv
iv = AES-GCM 使用的初始化向量IV 不需要保密。
但同一个 AES key 下,IV 不能重复。
常见做法是每次加密都随机生成一个 IV。
IV 要和 cipherText 一起发给后端。
因为后端解密时也需要它。
3. cipherText
cipherText = AES-GCM 加密后的业务数据比如原始请求是:
{ "username": "wu", "password": "123456" }经过 AES-GCM 后,就变成 cipherText。
后端拿到 AES key 和 iv 后,才能解密出原始请求体。
十、timestamp 和 nonce 有什么用?
很多加密请求里,还会带:
{ "timestamp": 1780000000000, "nonce": "random-string" }它们主要用于防重放。
因为即使请求体被加密,如果攻击者截获了一整包请求,理论上可能重复发送这包请求。
比如重复提交:
encryptedKey + iv + cipherText如果后端不做任何校验,可能无法区分这是新请求还是旧请求重放。
timestamp 和 nonce 的作用是:
timestamp: 判断请求是否过期,比如只允许 5 分钟内有效。 nonce: 随机字符串,后端记录已使用 nonce,防止同一请求重复提交。所以更完整的业务层加密请求通常会考虑:
加密 防篡改 防重放 过期控制AES-GCM 本身可以提供密文完整性校验。
timestamp + nonce 可以进一步防止重放。
十一、请求级混合加密
上面讲的是一种常见方案:
每个请求都生成一个临时 AES key这叫请求级混合加密。
流程是:
每次请求: 生成 AES key AES 加密数据 RSA 加密 AES key 后端 RSA 解 AES key 后端 AES 解数据优点:
简单 无状态 每个请求独立 后端不用保存会话 AES key缺点:
每个请求都要 RSA 解密 性能成本更高 请求体更大适合:
登录 改密码 支付 提现 绑定银行卡 提交隐私信息 设备控制指令也就是敏感接口。
十二、会话级混合加密
如果所有接口都要求业务层加密,每个请求都 RSA 一次,成本就比较高。
这时可以考虑会话级混合加密。
流程是:
1. App 启动或登录前,请求后端公钥 2. App 生成 sessionAesKey 3. App 用 RSA 公钥加密 sessionAesKey 4. 后端 RSA 私钥解密,拿到 sessionAesKey 5. 后端生成 secureSessionId 6. 后端把 secureSessionId -> sessionAesKey 存 Redis,设置过期时间 7. 后续请求 Header 带 secureSessionId 8. 后续请求体都用 sessionAesKey 做 AES-GCM 加密也就是:
第一次用 RSA 交换 AES key 后续全部用 AES 加密业务数据优点:
性能更好 适合全接口加密 后续请求体更小缺点:
后端要维护 secureSession 要处理 session 过期 要处理重连 要处理多设备 要处理 Redis 丢失 设计复杂度更高适合:
金融类 App 政企系统 IoT 设备控制 全接口业务层加密场景普通业务系统不一定需要做到这一步。
十三、是不是所有接口都要 AES + RSA 加密?
不一定。
这要看业务安全等级。
1. 普通 App
普通 App 通常采用:
HTTPS Token 认证 服务端密码哈希 Token 本地加密存储 日志脱敏就已经能覆盖大部分场景。
不一定需要所有接口再做 AES + RSA 业务加密。
2. 中等安全 App
可以只对敏感接口加密,比如:
登录 注册 改密码 支付 提现 绑定银行卡 提交身份证 提交隐私信息 设备控制指令这时可以采用请求级混合加密。
3. 高安全 App
如果业务要求所有接口都不能以明文 JSON 形式出现在业务层,可以做全接口加密。
这时更适合会话级混合加密。
比如:
先建立 secureSession 后续所有请求体 AES-GCM 加密十四、AES + RSA 和 HTTPS 是什么关系?
这是最容易混淆的点。
HTTPS 解决的是:
传输层安全AES + RSA 业务加密解决的是:
业务层请求体加密可以这样理解:
HTTPS: 保护 App 到服务器之间的网络传输通道。 AES + RSA: 即使在业务层看到请求体,也看到的是密文。普通项目里,HTTPS 已经是必须的。
即使做了 AES + RSA,也不能替代 HTTPS。
不要这样想:
我做了 AES + RSA,所以可以不用 HTTPS。这是错误的。
正确理解是:
HTTPS 是底线。 AES + RSA 是在 HTTPS 之上的额外业务层保护。所以完整结构是:
业务 JSON ↓ AES-GCM 业务层加密 业务密文 ↓ HTTPS/TLS 传输层加密 网络传输这是两层不同的保护。
十五、AES + RSA 和 Token 本地加密有什么区别?
这也是很容易混的点。
前面移动端登录态安全篇里讲过:
Token 本地存储: AES-GCM + Android Keystore这里又讲:
接口加密: AES-GCM + RSA它们看起来都用了 AES-GCM,但不是一回事。
1. Token 本地加密
目标是:
保护 App 本地保存的 Token结构是:
Android Keystore ↓ 保护本地 AES key AES-GCM ↓ 加密 accessToken / refreshToken DataStore / MMKV ↓ 保存 Token 密文这里的 AES key 是本机本地使用。
后端不需要知道。
2. 接口业务层加密
目标是:
保护 App 和后端之间的业务请求体结构是:
App 生成临时 AES key ↓ AES-GCM 加密请求体 ↓ RSA 公钥加密 AES key ↓ 后端 RSA 私钥解 AES key ↓ 后端 AES-GCM 解请求体这里的 AES key 是 App 和后端通信使用。
后端必须知道。
所以要记住:
本地 Token 加密: AES key 由 Android Keystore 保护,后端不需要知道。 接口业务加密: AES key 需要通过 RSA 公钥加密后传给后端,后端需要解出来。它们不是同一把 AES key。
不要混用。
十六、App 端伪代码
下面用伪代码表达一次请求级混合加密。
data class EncryptRequest( val encryptedKey: String, val iv: String, val cipherText: String, val timestamp: Long, val nonce: String )App 加密流程:
fun encryptRequest(jsonBody: String, serverPublicKey: PublicKey): EncryptRequest { // 1. 随机生成 AES key val aesKey = generateRandomAesKey() // 2. 随机生成 IV val iv = generateRandomIv() // 3. AES-GCM 加密业务 JSON val cipherText = aesGcmEncrypt( plainText = jsonBody, key = aesKey, iv = iv ) // 4. RSA 公钥加密 AES key val encryptedKey = rsaEncrypt( data = aesKey.encoded, publicKey = serverPublicKey ) // 5. 组装请求 return EncryptRequest( encryptedKey = base64(encryptedKey), iv = base64(iv), cipherText = base64(cipherText), timestamp = System.currentTimeMillis(), nonce = randomNonce() ) }这里重点不是代码细节,而是流程:
先 AES 加密数据 再 RSA 加密 AES key十七、后端伪代码
后端解密流程:
public String decryptRequest(EncryptRequest request) { // 1. Base64 解码 encryptedKey byte[] encryptedKeyBytes = base64Decode(request.getEncryptedKey()); // 2. RSA 私钥解密 AES key byte[] aesKeyBytes = rsaDecrypt( encryptedKeyBytes, serverPrivateKey ); // 3. Base64 解码 iv 和 cipherText byte[] iv = base64Decode(request.getIv()); byte[] cipherText = base64Decode(request.getCipherText()); // 4. AES-GCM 解密请求体 String plainJson = aesGcmDecrypt( cipherText, aesKeyBytes, iv ); return plainJson; }解出来的 plainJson 才是原始业务请求体。
比如登录接口最终拿到:
{ "username": "wu", "password": "123456" }然后再进入正常业务逻辑。
十八、RSA 加密建议用什么填充?
工程里不要使用老旧不安全的 RSA 填充方式。
更推荐:
RSA/ECB/OAEPWithSHA-256AndMGF1Padding不要使用:
RSA/ECB/PKCS1Padding如果团队或平台兼容性有限,也要明确知道当前使用方案的安全边界和兼容成本。
同样,AES 建议使用:
AES/GCM/NoPadding而不是:
AES/ECB/PKCS5Padding基本原则是:
AES 不用 ECB。 RSA 优先 OAEP。 随机数使用安全随机数。 密钥不要写死在客户端。十九、公钥怎么给 App?
后端 RSA 公钥可以有几种方式给 App。
1. 内置在 App
App 里内置后端公钥。
优点:
简单 不需要额外接口缺点:
公钥轮换不方便 App 旧版本可能一直使用旧公钥2. 通过接口动态获取
比如:
GET /security/public-key后端返回:
{ "keyId": "v1", "publicKey": "xxxxx" }App 加密请求时带上:
{ "keyId": "v1", "encryptedKey": "xxx", "iv": "xxx", "cipherText": "xxx" }后端根据 keyId 找对应私钥解密。
优点:
支持公钥轮换 适合多版本密钥管理缺点:
需要考虑公钥接口防篡改 依然必须走 HTTPS这里再次强调:
公钥可以公开。 私钥绝对不能给客户端。二十、这套方案里谁应该保密?
需要保密的是:
RSA 私钥 服务端密钥材料 AES key 明文 Token 明文 用户密码不需要保密但要保证正确性的有:
RSA 公钥 IV keyId 算法版本 timestamp nonceIV 不需要保密,但不能乱用。
同一个 AES key 下,IV 不能重复。
timestamp 和 nonce 不一定保密,但后端要校验它们,防止重放。
二十一、加密后还要不要签名?
如果使用 AES-GCM,它本身提供认证加密能力,可以发现密文被篡改。
但在一些更复杂的接口安全体系里,可能还会额外做签名。
比如:
sign = HMAC-SHA256(encryptedKey + iv + cipherText + timestamp + nonce, secret)这样可以进一步明确请求完整性和身份认证。
不过不要一上来就堆很多东西。
普通项目先把这些基础做好:
HTTPS Token 认证 密码哈希 Token 本地加密 日志脱敏 必要敏感接口 AES + RSA如果业务安全要求更高,再设计完整签名、防重放、密钥轮换体系。
二十二、常见误区
误区 1:AES 和 RSA 二选一
不对。
它们不是二选一,而是分工合作。
AES 加密数据 RSA 加密 AES key误区 2:RSA 直接加密所有请求体
不推荐。
RSA 慢,而且有长度限制。
大数据加密交给 AES。
误区 3:AES key 写死在 App 里
错误。
APK 可以反编译,写死 key 风险很大。
误区 4:做了 AES + RSA 就不用 HTTPS
错误。
HTTPS 是底线。
AES + RSA 是业务层额外保护。
误区 5:Token 本地 AES key 和接口 AES key 是一回事
错误。
本地 Token 加密的 AES key 由 Android Keystore 保护,后端不需要知道。
接口加密的 AES key 需要通过 RSA 加密传给后端,后端要用它解密请求体。
误区 6:公钥不能给客户端
不对。
公钥可以给客户端。
私钥不能给客户端。
二十三、最终总结
AES 和 RSA 到底怎么配合?
一句话:
AES 加密业务数据,RSA 加密 AES 密钥。
为什么这样设计?
因为:
AES: 速度快,适合加密大量数据,但 key 传递是问题。 RSA: 适合保护少量数据,比如 AES key,但不适合加密大量业务数据。所以工程上通常是:
App 生成临时 AES key ↓ AES-GCM 加密请求体 ↓ RSA 公钥加密 AES key ↓ 后端 RSA 私钥解出 AES key ↓ 后端 AES-GCM 解出请求体再放到完整安全体系里:
密码存储: bcrypt / Argon2id / PBKDF2 登录传输: HTTPS 业务层接口加密: AES-GCM + RSA Token 本地存储: AES-GCM + Android Keystore 接口认证: accessToken / refreshToken Token 失效: Redis / jti / tokenVersion 日志安全: 敏感字段脱敏最后再记住一句:
HTTPS 是底线,AES + RSA 是业务层增强;Token 本地加密和接口业务加密不是一回事,不要混用同一套 key。
