在做 App 登录接口时,很多移动端开发都会有一个疑问:
用户输入的密码,客户端到底要不要先加密一下再传给后端?
以前很多项目里常见的做法是:
val passwordMd5 = md5(password)
api.login(username, passwordMd5)
看起来好像更安全,因为客户端没有直接把原始密码传给后端。
但真正从企业级认证体系来看,这个理解其实容易走偏。
更常见、更标准的做法是:
App 收集用户输入的原始密码
↓
通过 HTTPS/TLS 加密通道传给后端
↓
后端用 bcrypt / Argon2id / PBKDF2 校验密码
↓
登录成功后返回 accessToken / refreshToken
也就是说,App 登录时通常传的是用户输入的原始密码,但它不是通过 HTTP 明文裸奔,而是在 HTTPS/TLS 加密通道里传输。
这篇文章就专门讲清楚这个问题。
一、先把问题说清楚:什么叫"明文密码"?
很多人一听到:
客户端传原始密码
第一反应就是:
这不是明文传输吗?
这里要区分两个层次。
从业务代码角度看,登录请求体可能是这样的:
{
"username": "wu",
"password": "123456"
}
这确实是业务层的原始密码。
但是从网络传输角度看,只要走的是 HTTPS,这个请求体会在 TLS 通道里被加密传输。
所以不是:
App --HTTP明文--> 后端
而是:
App --HTTPS/TLS加密通道--> 后端
这两个概念一定要分清楚。
业务层看到的是原始密码,不代表网络层就是明文裸奔。
二、为什么不建议客户端先 MD5?
很多老项目会这么做:
val passwordMd5 = md5(password)
api.login(username, passwordMd5)
看起来好像安全了一点,因为网络请求里没有出现 123456。
但问题是:后端如果认可这个 MD5 值登录,那么这个 MD5 值本身就变成了"等价密码"。
比如:
原始密码:123456
MD5 后:e10adc3949ba59abbe56e057f20f883e
如果后端登录接口接收的是:
{
"username": "wu",
"password": "e10adc3949ba59abbe56e057f20f883e"
}
那么攻击者一旦拿到这个 MD5 值,就不需要知道原始密码 123456,直接提交这个 MD5 值也能登录。
这时你只是把:
123456
换成了:
e10adc3949ba59abbe56e057f20f883e
本质上并没有解决登录凭证泄漏的问题。
所以客户端 MD5 不是传输安全方案。
真正负责传输安全的是 HTTPS/TLS。
三、密码安全到底分成哪几段?
密码安全不能只看"客户端有没有加密"。
完整链路应该拆成三段。
1. 传输阶段
解决的问题是:
App 到后端之间,密码会不会被中间人看到?
这一段靠 HTTPS/TLS。
用户输入密码
↓
HTTPS/TLS 加密传输
↓
后端收到密码
所以登录接口必须强制 HTTPS,不能允许 HTTP 明文请求。
2. 后端验证阶段
后端收到密码后,不能把密码直接存起来。
正确做法是:
用户输入的密码
↓
后端使用 bcrypt / Argon2id / PBKDF2 做密码哈希校验
↓
和数据库里的 password_hash 比较
注意,这里不是"解密数据库密码"。
数据库里不应该保存用户原始密码。
3. 数据库存储阶段
数据库里不应该是:
password = 123456
也不推荐是:
password = MD5(123456)
而应该是类似:
password = bcrypt(123456)
或者:
password = Argon2id(123456)
这样即使数据库泄漏,攻击者拿到的也不是用户原始密码,而是一串经过密码哈希算法处理后的结果。
四、为什么密码后端不需要"还原"?
很多人会疑惑:
密码做了 MD5 / bcrypt 之后不是无法还原了吗?那登录时怎么验证?
答案是:密码验证不需要还原。
注册时:
用户密码:123456
↓
bcrypt 处理
↓
数据库保存 password_hash
登录时:
用户再次输入:123456
↓
后端用同样算法重新计算
↓
和数据库里的 password_hash 比较
如果匹配,说明密码正确。
也就是说:
密码安全存储的目标,就是让后端也无法还原用户密码。
所以正规系统通常不会提供"找回原密码",而是"重置密码"。
因为系统自己也不应该知道用户原始密码。
五、客户端到底应该做什么?
移动端在登录阶段的职责其实很明确。
客户端应该做:
1. 收集用户输入的账号密码
2. 通过 HTTPS 发给后端
3. 不在本地保存密码
4. 不打印密码日志
5. 不把密码写入崩溃上报
6. 不用 MD5 伪装成安全加密
客户端不应该做:
1. 把密码存在 SharedPreferences / MMKV / SQLite
2. 把密码打印到 Logcat
3. 把密码放到 URL 参数里
4. 自己做 MD5 后当成登录密码
5. 把所谓加密密钥写死在 APK 里
错误示例:
POST /login?username=wu&password=123456
密码不要放 URL。
推荐:
POST /login
Content-Type: application/json
{
"username": "wu",
"password": "123456"
}
并且必须走 HTTPS。
六、后端到底应该做什么?
后端才是密码安全的核心责任方。
后端应该做:
1. 强制登录接口走 HTTPS
2. 不保存明文密码
3. 使用 bcrypt / Argon2id / PBKDF2 存储密码哈希
4. 登录时使用 PasswordEncoder.matches() 这类安全比较方式
5. 不打印登录请求里的 password
6. 登录失败做限流 / 锁定 / 验证码 / 风控
7. 登录成功后签发 accessToken 和 refreshToken
Spring Security 中常见做法是:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
注册时:
String encodedPassword = passwordEncoder.encode(rawPassword);
user.setPassword(encodedPassword);
登录时:
boolean matched = passwordEncoder.matches(
loginRequest.getPassword(),
user.getPassword()
);
这里的 matches() 不是解密,而是重新计算并比较。
七、那 AES + RSA 登录加密有没有意义?
有,但不是普通项目的第一选择。
普通项目通常是:
账号密码
↓
HTTPS
↓
后端
这已经解决传输加密问题。
但是在一些高安全场景,比如金融、支付、政企、医疗、设备控制等业务中,可能会在 HTTPS 外再做一层业务加密。
这时可以使用 AES + RSA 的混合加密。
流程大概是:
App 生成临时 AES key
↓
AES-GCM 加密登录请求体
↓
RSA 公钥加密 AES key
↓
把 encryptedKey + iv + cipherText 发给后端
↓
后端 RSA 私钥解出 AES key
↓
后端 AES-GCM 解出登录请求体
也就是:
AES 负责加密数据
RSA 负责加密 AES key
但这属于业务层额外加密,不是用来替代 HTTPS 的。
即使做了 AES + RSA,也仍然要走 HTTPS。
八、为什么 HTTPS 已经做了很多事?
HTTPS/TLS 本身底层就有类似思想:
先通过非对称加密 / 密钥交换协商出会话密钥
↓
后续大量通信使用对称加密算法传输数据
也就是说,HTTPS 已经在传输层做了:
密钥协商
身份校验
数据加密
完整性保护
所以普通 App 登录接口不需要自己再造一套"客户端 RSA 加密密码"的轮子。
否则很容易出现:
密钥写死在客户端
加密参数设计错误
后端解密流程复杂
日志仍然泄漏密码
误以为有业务加密就可以不用 HTTPS
这些反而会制造新的安全问题。
九、真正要警惕的是日志泄漏
很多密码和 Token 泄漏,不是因为 AES、RSA、HTTPS 被破解,而是自己打日志打出去了。
比如客户端:
Log.d("Login", "password = $password")
或者 OkHttp 日志打印了完整请求体。
后端也可能打印:
LoginRequest(username=wu, password=123456)
这些都很危险。
登录相关日志应该做到:
password:永远不打印
accessToken:永远不打印
refreshToken:永远不打印
Authorization:脱敏
Cookie:脱敏
验证码:不打印
可以打印:
登录成功 / 登录失败
用户 ID
设备 ID
错误码
请求耗时
风险标签
但不要打印密码和 Token。
十、最终结论
App 登录时,密码到底要不要加密?
更准确的答案是:
客户端通常不需要自己对密码做 MD5 / bcrypt / RSA。
客户端应该通过 HTTPS 把用户输入的原始密码发给后端。
后端负责使用 bcrypt / Argon2id / PBKDF2 做密码验证和安全存储。
所以这件事的分工是:
客户端:
- 负责 HTTPS 传输
- 不保存密码
- 不打印密码
- 不用客户端 MD5 当安全方案
后端:
- 负责密码哈希存储
- 负责密码校验
- 负责登录失败风控
- 负责签发 Token
Token 阶段:
- 客户端负责安全保存 accessToken / refreshToken
- 后端负责 Token 校验、刷新和失效
一句话总结:
App 登录时传原始密码并不等于明文裸奔,前提是必须走 HTTPS;密码安全的重点不是客户端先 MD5,而是 HTTPS 传输 + 后端安全哈希 + 日志脱敏 + 登录风控。