173 lines
4.3 KiB
Markdown
173 lines
4.3 KiB
Markdown
|
|
# iOS 登录流程 —— iOS 端集成
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、iOS 需要的核心组件
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
AuthService ← 调后端登录接口
|
|||
|
|
UserService ← 用户信息管理
|
|||
|
|
TokenStore ← token 存储协议
|
|||
|
|
KeychainTokenStore ← 基于 Keychain 的安全存储实现
|
|||
|
|
AppSession ← 管理当前登录态
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、数据存储策略
|
|||
|
|
|
|||
|
|
| 数据 | 存储位置 | 生命周期 | 原因 |
|
|||
|
|
|------|---------|---------|------|
|
|||
|
|
| `accessToken` | 内存 | App 运行期间 | 短期使用,不需要持久化 |
|
|||
|
|
| `refreshToken` | Keychain | 长期持久化 | 敏感凭证,需安全存储,卸载后也保留 |
|
|||
|
|
| `user` | AppSession / UserStore | App 运行期间 | 用户展示信息 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、App 启动流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
App 启动
|
|||
|
|
→ AppSession.checkSession()
|
|||
|
|
→ 从 Keychain 读取 refreshToken
|
|||
|
|
→ 如果没有 refreshToken
|
|||
|
|
→ 进入登录页
|
|||
|
|
→ 如果有 refreshToken
|
|||
|
|
→ 调用 POST /api/auth/refresh
|
|||
|
|
→ 成功
|
|||
|
|
→ 存储新的 accessToken + refreshToken
|
|||
|
|
→ 调用 GET /api/users/me
|
|||
|
|
→ 存储 user 信息
|
|||
|
|
→ 进入主界面
|
|||
|
|
→ 失败
|
|||
|
|
→ 清空 Keychain + 内存 token
|
|||
|
|
→ 进入登录页
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、登录流程
|
|||
|
|
|
|||
|
|
### 开发登录(dev-login)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户在登录页输入邮箱/昵称
|
|||
|
|
→ AuthService.devLogin(email, nickname)
|
|||
|
|
→ POST /api/auth/dev-login
|
|||
|
|
→ 后端返回 { accessToken, refreshToken, user }
|
|||
|
|
→ refreshToken 存 Keychain
|
|||
|
|
→ accessToken 放内存
|
|||
|
|
→ user 放 AppSession
|
|||
|
|
→ 进入主界面
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Apple 登录
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户点击 Sign in with Apple
|
|||
|
|
→ iOS 系统弹出 Apple 授权界面
|
|||
|
|
→ 用户授权成功
|
|||
|
|
→ 拿到 identityToken + authorizationCode 等
|
|||
|
|
→ AuthService.appleLogin(identityToken, ...)
|
|||
|
|
→ POST /api/auth/apple
|
|||
|
|
→ 后端验证 Apple token,返回 { accessToken, refreshToken, user }
|
|||
|
|
→ refreshToken 存 Keychain
|
|||
|
|
→ accessToken 放内存
|
|||
|
|
→ user 放 AppSession
|
|||
|
|
→ 进入主界面
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、接口请求拦截
|
|||
|
|
|
|||
|
|
所有需要登录的接口都必须携带:
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
Authorization: Bearer {accessToken}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### HTTP Client 封装建议
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
所有请求自动注入 Authorization Header
|
|||
|
|
→ 从 AuthService 获取当前 accessToken
|
|||
|
|
→ 自动添加到请求头
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 401 自动处理
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
接口返回 401
|
|||
|
|
→ 调用 POST /api/auth/refresh
|
|||
|
|
→ 成功
|
|||
|
|
→ 更新 accessToken
|
|||
|
|
→ 自动重试原请求
|
|||
|
|
→ 失败
|
|||
|
|
→ 清空 Keychain + 内存数据
|
|||
|
|
→ 跳转登录页
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**重要**:重试原请求时注意避免无限循环,设置最多重试 1 次。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、退出登录
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户点击退出登录
|
|||
|
|
→ AuthService.logout()
|
|||
|
|
→ POST /api/auth/logout
|
|||
|
|
Body: { refreshToken: 从 Keychain 取的 refreshToken }
|
|||
|
|
Header: Authorization: Bearer accessToken
|
|||
|
|
→ 后端标记 refreshToken revoked
|
|||
|
|
→ iOS 端:
|
|||
|
|
→ 清除 Keychain 中的 refreshToken
|
|||
|
|
→ 清除内存中的 accessToken
|
|||
|
|
→ 清除 AppSession 中的 user
|
|||
|
|
→ 跳转登录页
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、Token 存储对比:UserDefaults vs Keychain
|
|||
|
|
|
|||
|
|
| | UserDefaults | Keychain |
|
|||
|
|
|------|------------|----------|
|
|||
|
|
| 安全性 | 低(明文存储) | 高(系统级加密) |
|
|||
|
|
| 应用卸载后 | 数据被清除 | 可选保留(推荐保留) |
|
|||
|
|
| 备份 | 包含在 iTunes/iCloud 备份中 | 仅加密备份 |
|
|||
|
|
| 适用数据 | 非敏感偏好设置 | 密码、Token 等敏感凭据 |
|
|||
|
|
|
|||
|
|
**结论:refreshToken 一定要用 Keychain 存储。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、Session 状态机
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
App 启动
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
┌─────────────────┐
|
|||
|
|
│ 检查 Keychain │
|
|||
|
|
│ 有 refreshToken? │
|
|||
|
|
└───────┬─────────┘
|
|||
|
|
│
|
|||
|
|
┌───────┴───────┐
|
|||
|
|
│ 有 │ 无
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────┐ ┌──────────┐
|
|||
|
|
│ 调 refresh │ │ 进入登录页 │
|
|||
|
|
│ 接口 │ └──────────┘
|
|||
|
|
└─────┬─────┘
|
|||
|
|
│
|
|||
|
|
┌────┴────┐
|
|||
|
|
│ 成功 │ 失败
|
|||
|
|
▼ ▼
|
|||
|
|
┌────────┐ ┌──────────┐
|
|||
|
|
│ 调 /me │ │ 清空数据 │
|
|||
|
|
│ 进主页 │ │ 进登录页 │
|
|||
|
|
└────────┘ └──────────┘
|
|||
|
|
```
|