在现代Web应用中,Token过期导致的401错误是影响用户体验的关键问题。本文将手把手实现一套完整的Token自动刷新机制,覆盖从原理到实战的全过程。
一、为什么需要Token自动刷新?
当用户使用应用时,会遇到两种典型场景:
- 场景1:用户正在填写复杂表单时Token突然过期,提交时被迫重新登录
- 场景2:用户长期不操作后返回应用,每个操作都要求重新认证
传统方案(仅Access Token)的局限性:
// 传统方案伪代码
fun requestData() {try {// 直接使用可能过期的Tokenval data = api.getData() } catch (e: UnauthorizedException) {// 强制跳转登录navigateToLogin()}
}
二、双Token自动刷新机制设计
核心架构图
双Token策略对比
令牌类型 | 有效期 | 存储位置 | 用途 |
---|---|---|---|
Access Token | 短(2h) | 内存/LocalStorage | API请求鉴权 |
Refresh Token | 长(7d) | HttpOnly Cookie | 获取新Access Token |
三、Kotlin全栈实现(前端+后端)
前端实现(Kotlin/JS + Ktor Client)
// TokenManager.kt
object TokenManager {private var accessToken: String? = nullprivate const val REFRESH_TOKEN_KEY = "refresh_token"// 获取Access Tokenfun getAccessToken(): String {return accessToken ?: throw IllegalStateException("Token not initialized")}// 获取Refresh Token(从Cookie)fun getRefreshToken(): String {return document.cookie.split("; ").firstOrNull { it.startsWith("$REFRESH_TOKEN_KEY=") }?.substringAfter("=") ?: throw IllegalStateException("No refresh token")}// 保存新Tokenfun saveNewTokens(access: String, refresh: String) {accessToken = access// 安全设置Refresh Token Cookiedocument.cookie = "$REFRESH_TOKEN_KEY=$refresh; " +"max-age=${7 * 24 * 3600}; " +"path=/; " +"secure; " +"samesite=strict"}
}
// ApiClient.kt
class ApiClient {private val client = HttpClient {install(JsonFeature) { serializer = KotlinxSerializer() }install(DefaultRequest) { header(HttpHeaders.ContentType, "application/json") }}private var isRefreshing = falseprivate val failedQueue = mutableListOf<suspend () -> Unit>()// 请求拦截器private fun addAuthHeader(request: HttpRequestBuilder) {request.headers {append(HttpHeaders.Authorization, "Bearer ${TokenManager.getAccessToken()}")}}// 核心刷新逻辑private suspend fun handleUnauthorized(originalRequest: HttpRequestBuilder): Response {if (isRefreshing) {// 将请求加入队列等待刷新完成return suspendCoroutine { continuation ->failedQueue.add { continuation.resumeWith(runCatching { client.request(originalRequest) })}}}isRefreshing = truereturn try {// 刷新Tokenval newTokens = refreshToken()TokenManager.saveNewTokens(newTokens.accessToken, newTokens.refreshToken)// 重试所有队列中的请求failedQueue.forEach { it.invoke() }failedQueue.clear()// 重试原始请求addAuthHeader(originalRequest)client.request(originalRequest)} catch (e: Exception) {// 刷新失败处理TokenManager.clearTokens()redirectToLogin()throw e} finally {isRefreshing = false}}// 刷新Token请求private suspend fun refreshToken(): TokenResponse {return client.post("https://api.example.com/auth/refresh") {setBody(RefreshRequest(TokenManager.getRefreshToken()))}}// 封装的API请求方法suspend inline fun <reified T> get(url: String,block: HttpRequestBuilder.() -> Unit = {}): T {return client.request {method = HttpMethod.Geturl(this@ApiClient.url)addAuthHeader(this)block()}}// 其他HTTP方法封装...
}
后端实现(Ktor Server + JWT)
// AuthRoutes.kt
fun Route.authRoutes() {route("/auth") {// 刷新Token接口post("/refresh") {val request = call.receive<RefreshRequest>()val refreshToken = request.refreshToken// 验证Refresh Token有效性val userId = validateRefreshToken(refreshToken)if (userId == null) {call.respond(HttpStatusCode.Forbidden)return@post}// 生成新Access Tokenval newAccessToken = generateAccessToken(userId)// 可选:生成新Refresh Token(轮转策略)val newRefreshToken = generateRefreshToken(userId)// 返回新Tokencall.respond(TokenResponse(newAccessToken, newRefreshToken))}}
}// TokenUtils.kt
object TokenUtils {private val ACCESS_TOKEN_EXPIRY = 2.hoursprivate val REFRESH_TOKEN_EXPIRY = 7.daysprivate val SECRET = System.getenv("JWT_SECRET")// 生成Access Tokenfun generateAccessToken(userId: String): String {return JWT.create().withSubject(userId).withExpiresAt(Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRY.toMillis())).sign(Algorithm.HMAC256(SECRET))}// 验证Refresh Tokenfun validateRefreshToken(token: String): String? {return try {val verifier = JWT.require(Algorithm.HMAC256(SECRET)).build()verifier.verify(token).subject} catch (e: Exception) {null}}
}
四、进阶安全增强策略
1. Refresh Token轮转机制
// 服务端刷新逻辑增强
fun refreshToken(refreshToken: String): TokenResponse {val userId = validateRefreshToken(refreshToken) ?: return null// 检查Refresh Token是否已使用过(防止重放攻击)if (redis.exists("used_refresh:$refreshToken")) {// 检测到重放攻击,使该用户所有Token失效invalidateAllUserTokens(userId)return null}// 标记该Refresh Token已使用redis.setex("used_refresh:$refreshToken", 300, "1")// 生成新Token...
}
2. 并发请求处理优化
五、特殊场景处理方案
1. 静默刷新(后台定时刷新)
// 启动Token刷新定时器
fun startTokenRefreshScheduler() {val refreshThreshold = 15.minutes // 在过期前15分钟刷新CoroutineScope(Dispatchers.Default).launch {while (true) {delay(1.minutes) // 每分钟检查一次val tokenExpiry = getTokenExpiry() // 从Token解析过期时间if (tokenExpiry - System.currentTimeMillis() < refreshThreshold.toMillis()) {try {refreshTokenSilently()} catch (e: Exception) {// 静默刷新失败,下次再试}}}}
}// 静默刷新实现
private suspend fun refreshTokenSilently() {if (!isRefreshing) {val newTokens = apiClient.refreshToken()TokenManager.saveNewTokens(newTokens.accessToken, newTokens.refreshToken)}
}
2. 跨标签页同步Token状态
// Token同步管理器
object TokenSyncManager {private const val SYNC_EVENT = "token_updated"init {// 监听storage事件window.addEventListener("storage", { event ->if (event.key == "access_token") {TokenManager.accessToken = event.newValue}})}fun broadcastTokenUpdate() {// 触发storage事件通知其他标签页localStorage.setItem(SYNC_EVENT, System.currentTimeMillis().toString())}
}// 在保存新Token时调用
fun saveNewTokens(access: String, refresh: String) {// ...原有逻辑TokenSyncManager.broadcastTokenUpdate()
}
六、最佳实践总结
前端关键实践
- 预刷新机制:在Token过期前15-30分钟自动刷新
- 心跳检测:页面激活时检查Token有效期
- 错误降级:刷新失败时保留用户操作数据
// 预刷新示例
fun scheduleTokenRefresh(expiry: Instant) {val refreshTime = expiry.minus(15, ChronoUnit.MINUTES)// 设置定时刷新...
}
后端关键实践
- Refresh Token轮转:每次刷新后生成新Refresh Token
- 使用次数限制:单Refresh Token最多使用3次
- 黑名单机制:使被盗Token立即失效
// Token黑名单实现
fun invalidateToken(token: String) {val remainingTime = getTokenExpiry(token) - System.currentTimeMillis()redis.setex("blacklist:$token", remainingTime / 1000, "1")
}
安全增强矩阵
攻击类型 | 防御措施 | 实现方式 |
---|---|---|
Token窃取 | 短有效期+黑名单 | 2小时有效期,Redis记录失效Token |
重放攻击 | Refresh Token单次使用 | 每次刷新后使旧Token失效 |
XSS攻击 | HttpOnly Cookie | 设置Refresh Token为HttpOnly |
CSRF攻击 | SameSite=Strict | Cookie属性设置 |
七、性能优化指南
- 并发控制优化
// 使用Mutex替代标志锁
private val refreshMutex = Mutex()
private val failedQueue = Channel<suspend () -> Unit>(capacity = Channel.UNLIMITED)suspend fun handleUnauthorized(request: HttpRequestBuilder) {// 尝试获取锁,如果正在刷新则加入队列if (!refreshMutex.tryLock()) {return suspendCoroutine { cont ->failedQueue.trySend { cont.resumeWith(runCatching { executeRequest(request) }) }}}try {// 刷新Token...} finally {refreshMutex.unlock()// 处理队列...}
}
- 分布式系统下的Token管理
// 使用Redis存储Token状态
class RedisTokenStore(val redis: RedisClient) : TokenStore {override suspend fun isValid(token: String): Boolean {return redis.get("token_valid:$token") != null}override suspend fun invalidate(token: String) {val ttl = getRemainingTime(token) // 计算剩余时间redis.setex("token_valid:$token", ttl, "1")}
}
八、完整实现流程图
九、扩展思考:无感认证的未来
- 生物特征集成:结合FaceID/TouchID实现二次认证
fun authenticateWithBiometrics(): Boolean {return BiometricPrompt.authenticate(BiometricCriteria(strength = BiometricStrength.STRONG,allowedAuthenticators = Authenticators.BIOMETRIC_STRONG))
}
- 设备行为分析:基于用户行为模式动态调整Token有效期
动态策略示例:
- 常用设备:有效期7天
- 新设备:有效期2小时
- 异常行为:立即要求重新认证
- 量子安全Token:抗量子计算的认证协议
// 后量子加密示例
fun generateQuantumSafeToken(): String {return Falcon512.sign(userId + timestamp, quantumPrivateKey)
}
最佳实践提示:在金融/医疗等敏感场景,即使Token刷新成功,关键操作(如支付、修改密码)仍需进行二次认证
总结
Token自动刷新机制是现代Web应用的必备能力,核心在于:
- 双Token策略:Access Token短期有效,Refresh Token安全存储
- 错误无缝处理:401时自动刷新并重试请求
- 安全增强:HttpOnly、SameSite、Token轮转
- 极致体验:预刷新、后台静默刷新、跨标签同步
延伸阅读:
- OAuth 2.0安全最佳实践
- JWT RFC 7519标准
- Ktor官方认证文档