在现代Web应用中,Token过期导致的401错误是影响用户体验的关键问题。本文将手把手实现一套完整的Token自动刷新机制,覆盖从原理到实战的全过程。
一、为什么需要Token自动刷新?
当用户使用应用时,会遇到两种典型场景:
- 场景1:用户正在填写复杂表单时Token突然过期,提交时被迫重新登录
- 场景2:用户长期不操作后返回应用,每个操作都要求重新认证
传统方案(仅Access Token)的局限性:
kotlin
// 传统方案伪代码
fun requestData() {
try {
// 直接使用可能过期的Token
val data = api.getData()
} catch (e: UnauthorizedException) {
// 强制跳转登录
navigateToLogin()
}
}
二、双Token自动刷新机制设计
核心架构图
Client Server Auth Server API请求 (Access Token过期) 401 Unauthorized 发送Refresh Token 返回新Access Token 自动重试原请求 401/403错误 跳转登录页 alt [刷新成功] [刷新失败] Client Server Auth Server
双Token策略对比
令牌类型 | 有效期 | 存储位置 | 用途 |
---|---|---|---|
Access Token | 短(2h) | 内存/LocalStorage | API请求鉴权 |
Refresh Token | 长(7d) | HttpOnly Cookie | 获取新Access Token |
三、Kotlin全栈实现(前端+后端)
前端实现(Kotlin/JS + Ktor Client)
kotlin
// TokenManager.kt
object TokenManager {
private var accessToken: String? = null
private const val REFRESH_TOKEN_KEY = "refresh_token"
// 获取Access Token
fun 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")
}
// 保存新Token
fun saveNewTokens(access: String, refresh: String) {
accessToken = access
// 安全设置Refresh Token Cookie
document.cookie = "$REFRESH_TOKEN_KEY=$refresh; " +
"max-age=${7 * 24 * 3600}; " +
"path=/; " +
"secure; " +
"samesite=strict"
}
}
kotlin
// ApiClient.kt
class ApiClient {
private val client = HttpClient {
install(JsonFeature) { serializer = KotlinxSerializer() }
install(DefaultRequest) { header(HttpHeaders.ContentType, "application/json") }
}
private var isRefreshing = false
private 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 = true
return try {
// 刷新Token
val 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.Get
url([email protected])
addAuthHeader(this)
block()
}
}
// 其他HTTP方法封装...
}
后端实现(Ktor Server + JWT)
kotlin
// 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 Token
val newAccessToken = generateAccessToken(userId)
// 可选:生成新Refresh Token(轮转策略)
val newRefreshToken = generateRefreshToken(userId)
// 返回新Token
call.respond(TokenResponse(newAccessToken, newRefreshToken))
}
}
}
// TokenUtils.kt
object TokenUtils {
private val ACCESS_TOKEN_EXPIRY = 2.hours
private val REFRESH_TOKEN_EXPIRY = 7.days
private val SECRET = System.getenv("JWT_SECRET")
// 生成Access Token
fun generateAccessToken(userId: String): String {
return JWT.create()
.withSubject(userId)
.withExpiresAt(Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRY.toMillis()))
.sign(Algorithm.HMAC256(SECRET))
}
// 验证Refresh Token
fun 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轮转机制
kotlin
// 服务端刷新逻辑增强
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 401 刷新状态 请求2 401 等待队列 刷新完成 重试请求1 重试请求2
五、特殊场景处理方案
1. 静默刷新(后台定时刷新)
kotlin
// 启动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状态
kotlin
// 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有效期
- 错误降级:刷新失败时保留用户操作数据
kotlin
// 预刷新示例
fun scheduleTokenRefresh(expiry: Instant) {
val refreshTime = expiry.minus(15, ChronoUnit.MINUTES)
// 设置定时刷新...
}
后端关键实践
- Refresh Token轮转:每次刷新后生成新Refresh Token
- 使用次数限制:单Refresh Token最多使用3次
- 黑名单机制:使被盗Token立即失效
kotlin
// 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属性设置 |
七、性能优化指南
- 并发控制优化
kotlin
// 使用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管理
kotlin
// 使用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")
}
}
八、完整实现流程图
是 否 是 否 是 否 发起API请求 Token有效? 正常请求 正在刷新? 加入等待队列 发起刷新请求 刷新成功? 更新内存Token 跳转登录页 重试所有队列请求 刷新完成后触发
九、扩展思考:无感认证的未来
- 生物特征集成:结合FaceID/TouchID实现二次认证
kotlin
fun authenticateWithBiometrics(): Boolean {
return BiometricPrompt.authenticate(
BiometricCriteria(
strength = BiometricStrength.STRONG,
allowedAuthenticators = Authenticators.BIOMETRIC_STRONG
)
)
}
-
设备行为分析:基于用户行为模式动态调整Token有效期
动态策略示例:
- 常用设备:有效期7天
- 新设备:有效期2小时
- 异常行为:立即要求重新认证
-
量子安全Token:抗量子计算的认证协议
kotlin
// 后量子加密示例
fun generateQuantumSafeToken(): String {
return Falcon512.sign(userId + timestamp, quantumPrivateKey)
}
最佳实践提示:在金融/医疗等敏感场景,即使Token刷新成功,关键操作(如支付、修改密码)仍需进行二次认证
总结
Token自动刷新机制是现代Web应用的必备能力,核心在于:
- 双Token策略:Access Token短期有效,Refresh Token安全存储
- 错误无缝处理:401时自动刷新并重试请求
- 安全增强:HttpOnly、SameSite、Token轮转
- 极致体验:预刷新、后台静默刷新、跨标签同步
延伸阅读: