密码与加密基础篇(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 nonce

IV 不需要保密,但不能乱用。

同一个 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。