基于 RSA 非对称加密与挑战码机制的前端登录安全方案
一、背景与目标
在传统登录实现中,即使使用 HTTPS 进行传输加密,仍存在以下潜在风险:
- 代理或中间件解密 HTTPS 流量后,明文密码暴露在中间层
- 攻击者截获合法请求后进行重放攻击
- 数据库中密码以弱哈希形式存储,泄露后易被彩虹表破解
本方案通过引入 RSA 非对称加密与一次性挑战码机制,在现有 SHA256 密码存储体系下,构建一套兼具传输安全与防重放能力的前端登录流程。
二、方案概述
整体方案由以下三个核心机制组成:
- SHA256 哈希:与数据库存储格式保持一致,避免明文密码在网络中传输
- RSA-OAEP 非对称加密:使用公钥对登录凭证加密,只有持有私钥的服务端才能解密
- 一次性挑战码(Challenge):每次登录请求绑定唯一挑战码,验证后立即销毁,从根本上防止重放攻击
三、完整登录流程
3.1 获取挑战码
前端在发起登录请求前,首先向服务端请求一次性挑战码:
GET /auth/challenge
服务端生成随机 challengeId 与对应的 challenge 值,存入 Redis 并设置 30 秒 TTL,随后返回给前端。
3.2 前端加密处理(两次加密)
前端收到挑战码后,依次执行两步加密操作:
第一步:SHA256 哈希密码
sha256Hash = SHA256(用户明文密码)
此步骤将密码转换为与数据库存储值一致的哈希,避免明文密码在任何环节暴露。
第二步:RSA-OAEP 公钥加密
ciphertext = RSA_OAEP_encrypt(sha256Hash + ":" + challengeId, 公钥)
将哈希结果与 challengeId 以冒号拼接后整体加密。公钥内置于前端代码中,不动态下发,防止中间人替换公钥。加密算法必须使用 RSA-OAEP(配合 SHA-256),严禁使用存在 Bleichenbacher 攻击漏洞的 PKCS#1 v1.5。
3.3 发送登录请求
前端仅发送用户名与密文,challengeId 隐藏在密文内部,不在请求体中明文暴露:
POST /auth/login
{
"username": "alice",
"ciphertext": "<RSA加密后的密文>"
}
3.4 服务端验证流程
服务端收到请求后,按以下步骤进行验证:
① RSA 私钥解密
plaintext = RSA_私钥解密(ciphertext)
[sha256Hash, challengeId] = plaintext.split(":")
② 校验挑战码有效性
从 Redis 中查询 challengeId 对应记录,若不存在或已过期则拒绝请求;查询成功后立即删除该记录,确保挑战码一次性不可复用。
③ 校验密码哈希
将解密得到的 sha256Hash 与数据库中存储的哈希值进行比对,一致则验证通过。
④ 下发 Token
验证通过后生成 JWT Token 返回给前端,完成登录。
四、安全性分析
| 攻击场景 | 防护效果 | 说明 |
|---|---|---|
| 网络抓包重放 | ✅ 有效防护 | 密文不可复用,challengeId 一次性销毁 |
| 中间人篡改请求 | ✅ 有效防护 | 无私钥无法构造有效密文 |
| HTTPS 代理解密后重放 | ✅ 有效防护 | 挑战码已失效,重放请求被拒绝 |
| 数据库泄露 | ⚠️ 部分风险 | SHA256 无盐存储仍有彩虹表风险,建议迁移至 BCrypt |
| 服务端私钥泄露 | ❌ 全线失效 | 私钥需严格保管,建议配合 HSM 使用 |
五、关键设计要点
- RSA 密钥长度:最低 2048 bit,推荐 4096 bit
- 加密模式:必须使用 RSA-OAEP + SHA-256,禁止 PKCS#1 v1.5
- 公钥分发:内置于前端构建产物,不通过接口动态下发
- 挑战码生命周期:有效期 30 秒,验证后立即从 Redis 删除
- challengeId 隐藏:包含在密文中,不在请求参数中明文暴露
- 时序安全:服务端比较哈希时使用恒定时间比较函数,防止时序攻击
六、后续优化建议
- 密码存储迁移:将现有 SHA256 存储升级为 BCrypt 或 Argon2,加入随机盐值,从根本上消除彩虹表威胁
- 私钥管理:将 RSA 私钥存储于 HSM(硬件安全模块)或 KMS(密钥管理服务),避免私钥直接落盘
- 登录限频:结合 IP 限流与账号锁定策略,防止暴力破解
- 长期演进:如需彻底消除服务端接触密码哈希,可评估引入 SRP(Secure Remote Password)协议