4. 登录流程核心实现

登录是账号体系的入口,也是客户端与服务端交互最密集、最容易出稳定性问题的环节。本节围绕三种主流登录方式——账号密码登录、验证码登录、一键登录——拆解它们的交互链路、登录态生成方式、Token 下发结构,以及客户端本地安全存储的落地做法。三种方式共享同一套“凭证校验 → 登录态生成 → Token 下发 → 本地存储 → 后续请求携带”的主干,差异集中在凭证类型与校验路径上。

一、三种登录方式的交互链路差异

账号密码登录的凭证是“账号 + 密码”。客户端不应直接明文传输密码,通常做法是:客户端先请求服务端下发一次性随机数(nonce)或盐值,再用 SHA-256(password + nonce) 生成摘要上传。服务端用同样算法比对存储的密码哈希(如 bcrypt 结果),校验通过后进入登录态生成阶段。这条链路的关键点是防重放:nonce 必须一次性有效,过期时间建议 60 秒。

验证码登录的凭证是“手机号 + 短信验证码”。链路分两步:先调用发送接口,服务端生成 6 位数字验证码,写入 Redis 并设置 5 分钟 TTL,同时按手机号维度限流(如 60 秒内只能发一次、单日上限 10 次);用户提交后,服务端比对 Redis 中的值,一致则删除该 key 并进入登录态生成。验证码登录天然免密,但必须防止“验证码爆破”,因此校验失败次数超过 5 次应直接锁定该手机号 15 分钟。

一键登录依赖运营商网关能力。客户端集成运营商 SDK 后,在 4G/5G 蜂窝网络下可获取本机号码掩码(如 138****8888)和一个临时 token。客户端把该 token 传给业务服务端,业务服务端再向运营商服务端换取真实手机号,校验通过后直接生成登录态。一键登录的体验最好,但受网络环境限制(WiFi 下通常不可用),因此工程上一般把它作为首选,失败时降级到验证码登录。

三种登录方式交互链路与登录态生成 客户端 业务服务端 存储 / 外部 ① 请求 nonce 账号密码登录 POST /auth/nonce 生成 nonce TTL 60s 写入 Redis Redis nonce:xxx ② 提交摘要 SHA256(pwd+nonce) POST /auth/login 校验 nonce + 密码 bcrypt 比对 查询用户表 MySQL user 表 ③ 发送验证码 手机号 + 验证码登录 POST /sms/send 生成 6 位码 限流 60s / 日 10 次 写入 Redis TTL 5min Redis sms:138xxx ④ 运营商 token 一键登录 POST /onekey/login 换取真实手机号 运营商 API 校验 token 运营商网关 蜂窝网络 ⑤ 登录态生成 Access + Refresh 下发 Token 签发 JWT Access 2h / Refresh 30d 存储 Refresh Redis refresh:uid

二、登录态生成与 Token 下发结构

三种方式校验通过后,服务端统一进入登录态生成。推荐采用双 Token 结构:Access Token 有效期短(示例取 2 小时),用于业务接口鉴权;Refresh Token 有效期长(示例取 30 天),仅用于换取新的 Access Token。Access Token 用 JWT 承载,payload 中放入 uid、deviceId、iat、exp,服务端用 RS256 私钥签名,各业务服务用公钥验签,避免每次请求都查库。

Refresh Token 不放进 JWT,而是生成一个 32 字节随机串,服务端以 refresh:{uid}:{deviceId} 为 key 存入 Redis,value 记录签发时间与设备信息,TTL 设为 30 天。这样做的目的是支持“单设备登出”和“全端下线”:删除对应 key 即可让 Refresh Token 失效。Access Token 过期后,客户端用 Refresh Token 调用 /auth/refresh,服务端校验 Redis 中是否存在该 key,存在则签发新 Access Token,并可选择同时轮换 Refresh Token(旧 key 删除、新 key 写入),降低泄露风险。

Token 下发的响应体建议统一结构,便于客户端解析:

{
  "code": 0,
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiIs...",
    "refreshToken": "a3f8c2e1b9d74056...",
    "expiresIn": 7200,
    "tokenType": "Bearer",
    "uid": "100238",
    "isNewUser": false
  }
}

其中 expiresIn 单位为秒,客户端据此计算本地过期时间,提前 5 分钟触发静默刷新,避免请求因 Token 过期而失败。

三、本地安全存储方案

Token 落到客户端后,存储位置直接决定安全性。Android 上常见方案有三档:

  • SharedPreferences 明文:最不安全,root 设备可直接读取,仅适合临时调试。
  • EncryptedSharedPreferences:基于 Android Keystore 生成 AES 密钥,对 key/value 自动加解密,API 23+ 可用,是当前推荐的基础方案。
  • Keystore + 自管理加密:用 Keystore 生成不可导出的 RSA/AES 密钥,自行加密后写入文件或 SP,灵活度最高,适合对安全要求更高的场景。

工程上建议:Access Token 放内存 + EncryptedSharedPreferences 双写,内存优先,减少磁盘读取;Refresh Token 只放 EncryptedSharedPreferences,且不随日志输出。同时注意:不要把 Token 写进 BuildConfig、strings.xml 或日志;不要在 WebView 的 localStorage 中存 Token,避免 XSS 窃取。

若应用需要支持多进程(如推送进程),EncryptedSharedPreferences 在多进程下可能读到空值,此时应改用 Keystore 加密后写入独立文件,并通过 ContentProvider 或 AIDL 做跨进程访问。

四、稳定性与异常处理要点

登录链路的稳定性问题大多集中在“并发刷新”和“Token 失效”两类。并发刷新指多个请求同时发现 Access Token 过期,同时发起刷新,导致 Refresh Token 被多次轮换而互相失效。解决办法是在客户端加一把单飞锁(如 Mutex 或 synchronized),只允许一个刷新请求在途,其余请求挂起等待刷新结果。

Token 失效则要区分场景:Access Token 过期走静默刷新;Refresh Token 也失效(如被服务端主动删除)时,必须清空本地凭证并跳转登录页,同时避免在登录页重复弹出提示。服务端返回 401 时,客户端应统一拦截,先尝试刷新一次,刷新失败再登出,而不是每个接口各自处理。

最后,登录接口本身要有防刷与可观测性:按 IP + 账号维度限流,记录登录成功/失败日志(含设备指纹、时间、结果码),对连续失败账号触发风控。这样三种登录方式才能在共享主干的同时,各自守住自己的风险点。