┌─────────┐ 1. 用戶名/密碼 ┌─────────┐
│ 客戶端 │ ─────────────────────> │ 服務端 │
│ (App) │ │ (Server)│
└─────────┘ └────┬────┘
│
2. 驗證成功
生成 Token (JWT/隨機字符串)
關聯用戶 ID、設備信息、過期時間
存入 Redis/DB (Key: Token, Value: UserInfo)
│
3. 返回 Token
┌─────────┐ 4. 保存 Token ┌────┴────┐
│ 客戶端 │ <───────────────────── │ 服務端 │
│ (App) │ (HTTP Header/Body) │ (Server)│
└────┬────┘ └─────────┘
│
│ 5. 本地持久化存儲
▼
┌─────────────────────────────────────┐
│ Android: SharedPreferences │
│ iOS: Keychain (安全) / UserDefaults │
│ Key: "auth_token", Value: "xxx.yyy.zzz" │
└─────────────────────────────────────┘
┌─────────┐ 6. 攜帶 Token 請求 ┌─────────┐
│ 客戶端 │ ─────────────────────────> │ 服務端 │
│ (App) │ Header: Authorization: │ (Server)│
└─────────┘ Bearer └────┬────┘
│
7. 驗證 Token
Redis/DB 查找
檢查過期、撤銷、設備匹配
│
8. 返回業務數據
┌─────────┐ 9. 正常響應 ┌────┴────┐
│ 客戶端 │ <───────────────────── │ 服務端 │
│ (App) │ │ (Server)│
└─────────┘ └─────────┘
| 類型 | 生成方式 | 驗證方式 | 優點 | 缺點 | 推薦場景 |
|---|---|---|---|---|---|
| JWT (JSON Web Token) | 簽名算法 (HS256/RS256) 簽署 Payload | 本地驗證簽名 + 解析 Payload (無狀態) | 無需存儲、攜帶用戶信息、跨服務驗證 | 無法主動撤銷 (需黑名單)、Payload 體積大 | 微服務、單點登錄、無狀態架構 |
| 隨機字符串 (Opaque Token) | CSPRNG 生成 32+ 字節十六進制/Base64 | Redis/DB 查找 Key (有狀態) | 可隨時撤銷、Token 短、信息不洩露 | 需存儲、查詢延遲、集群需共享存儲 | 傳統單體、安全要求高、需強制下線 |
| UUID + 設備指紋 | UUID v4 + 設備信息哈希 | DB 查找 + 設備匹配 | 綁定設備、防盜用 | 換設備需重新登錄 | 金融/高安全 App |
// 保存 Token
fun saveToken(token: String) {
val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
prefs.edit().putString("access_token", token).apply()
// Refresh Token 存 KeyChain
KeyChainManager.save("refresh_token", refreshToken)
}
// 請求攔截器
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val token = getAccessToken()
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
chain.proceed(request)
}
.addInterceptor(RefreshTokenInterceptor()) // 401 自動刷新
.build()
// 401 自動刷新邏輯
class RefreshTokenInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 401) {
val newToken = refreshAccessTokenSync() // 同步調用刷新接口
if (newToken != null) {
val newRequest = chain.request().newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(newRequest)
}
}
return response
}
}
// KeyChain 存儲 (安全)
func saveToken(_ token: String, forKey key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
SecItemDelete(query as CFDictionary) // 先刪舊
SecItemAdd(query as CFDictionary, nil)
}
// Alamofire 請求適配器
class AuthRequestAdapter: RequestAdapter {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var request = urlRequest
if let token = KeyChainManager.get("access_token") {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return request
}
}
// 401 重試邏輯
let retryPolicy = RetryPolicy(retryLimit: 1) { (request, response, error) in
if response?.statusCode == 401 {
return AuthService.refreshToken().map { _ in true } ?? false
}
return false
}
# Access Token (短期)
Key: "access_token:{token}"
Value: JSON {userId, deviceId, permissions, exp}
TTL: 1800s (30min)
# Refresh Token (長期)
Key: "refresh_token:{token}"
Value: JSON {userId, deviceId, accessTokenKey, exp}
TTL: 2592000s (30d)
# 用戶所有 Token 索引 (便於強制下線)
Key: "user_tokens:{userId}"
Value: Set {access_token_1, refresh_token_1, ...}
# 黑名單 (JWT 無狀態撤銷)
Key: "token_blacklist:{token}"
Value: "1"
TTL: 同 Token 剩余過期時間
func checkAutoLogin() {
guard let refreshToken = KeyChainManager.get("refresh_token") else {
showLoginScreen(); return
}
// 靜默嘗試刷新 Access Token
AuthService.refreshToken(refreshToken) { result in
switch result {
case .success(let newAccessToken):
saveToken(newAccessToken)
enterMainApp()
case .failure:
// Refresh Token 過期/失效,清理本地跳轉登錄
clearLocalAuth()
showLoginScreen()
}
}
}
Token 自動登錄核心:**短 Access Token + 長 Refresh Token + 安全存儲 + 自動刷新攔截器 + 設備綁定 + 撤銷機制**。服務端推薦有狀態隨機 Token + Redis (可撤銷、安全),微服務架構可用 JWT + Redis 黑名單。客戶端必須實現401 靜默刷新與啟動檢查 Refresh Token邏輯,實現真正的「一次登錄,長期免輸入」。