在现代Web应用中,Token过期导致的401错误是影响用户体验的关键问题。本文将手把手实现一套完整的Token自动刷新机制,覆盖从原理到实战的全过程。

一、为什么需要Token自动刷新?

当用户使用应用时,会遇到两种典型场景:

  • 场景1:用户正在填写复杂表单时Token突然过期,提交时被迫重新登录
  • 场景2:用户长期不操作后返回应用,每个操作都要求重新认证

传统方案(仅Access Token)的局限性:

// 传统方案伪代码
fun requestData() {try {// 直接使用可能过期的Tokenval 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)内存/LocalStorageAPI请求鉴权
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 401
刷新状态
请求2 401
等待队列
刷新完成
重试请求1
重试请求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()
}

六、最佳实践总结

前端关键实践
  1. 预刷新机制:在Token过期前15-30分钟自动刷新
  2. 心跳检测:页面激活时检查Token有效期
  3. 错误降级:刷新失败时保留用户操作数据
// 预刷新示例
fun scheduleTokenRefresh(expiry: Instant) {val refreshTime = expiry.minus(15, ChronoUnit.MINUTES)// 设置定时刷新...
}
后端关键实践
  1. Refresh Token轮转:每次刷新后生成新Refresh Token
  2. 使用次数限制:单Refresh Token最多使用3次
  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=StrictCookie属性设置

七、性能优化指南

  1. 并发控制优化
// 使用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()// 处理队列...}
}
  1. 分布式系统下的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")}
}

八、完整实现流程图

发起API请求
Token有效?
正常请求
正在刷新?
加入等待队列
发起刷新请求
刷新成功?
更新内存Token
跳转登录页
重试所有队列请求
刷新完成后触发

九、扩展思考:无感认证的未来

  1. 生物特征集成:结合FaceID/TouchID实现二次认证
fun authenticateWithBiometrics(): Boolean {return BiometricPrompt.authenticate(BiometricCriteria(strength = BiometricStrength.STRONG,allowedAuthenticators = Authenticators.BIOMETRIC_STRONG))
}
  1. 设备行为分析:基于用户行为模式动态调整Token有效期
动态策略示例:
- 常用设备:有效期7天
- 新设备:有效期2小时
- 异常行为:立即要求重新认证
  1. 量子安全Token:抗量子计算的认证协议
// 后量子加密示例
fun generateQuantumSafeToken(): String {return Falcon512.sign(userId + timestamp, quantumPrivateKey)
}

最佳实践提示:在金融/医疗等敏感场景,即使Token刷新成功,关键操作(如支付、修改密码)仍需进行二次认证

总结

Token自动刷新机制是现代Web应用的必备能力,核心在于:

  1. 双Token策略:Access Token短期有效,Refresh Token安全存储
  2. 错误无缝处理:401时自动刷新并重试请求
  3. 安全增强:HttpOnly、SameSite、Token轮转
  4. 极致体验:预刷新、后台静默刷新、跨标签同步

延伸阅读

  • OAuth 2.0安全最佳实践
  • JWT RFC 7519标准
  • Ktor官方认证文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/84453.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/84453.shtml
英文地址,请注明出处:http://en.pswp.cn/web/84453.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《解构线性数据结构的核心骨架:从存储模型到操作范式的深度解析》

线性数据结构概述 线性数据结构是数据元素按线性顺序排列的集合,每个元素有唯一的前驱和后继(除首尾元素)。常见类型包括数组、队列、链表和栈,每种结构在存储和操作上具有独特特性。 线性表:顾名思义,线性表就是数据排成像一条线的结构。每个线性表上的数据最多只有前和后…

HW蓝队工作流程

HW蓝队工作流程 由多领域安全专家组成攻击队&#xff0c;在保障业务系统安全的前提下&#xff0c;直接在真实网络环境开展对抗&#xff0c;对参演单位目标系进行可控、可审计的网络安全实战攻击&#xff0c;通过攻防演习检验参演单位的安全防护和应急处置能力&#xff0c;提高…

语音相关-浏览器的自动播放策略研究和websocket研究

策略详情 媒体参与度 AudioContext音频API的实现 new Audio音频API的实现 相关实践 网页端 使用new Audio创建的音频对象进行音频播放的时候&#xff0c;如果用户没有与页面进行交互&#xff0c;那么会报错如下&#xff1a; 使用AudioContext创建的对象播放音频&#xff0c;…

Linux操作系统网络服务模块一DHCP服务概述

前言&#xff1a; 在Linux网络服务体系架构中&#xff0c;​DHCP&#xff08;Dynamic Host Configuration Protocol&#xff09;​​ 作为核心服务之一&#xff0c;承担着局域网内主机网络参数动态分配的关键任务。其设计初衷是解决传统手动配置IP地址的效率瓶颈与错误风…

FPGA基础 -- Verilog语言要素之变量类型

Verilog 变量类型&#xff08;Variable Types&#xff09; 一、什么是变量类型&#xff1f; 在 Verilog 中&#xff0c;变量类型用于保存过程赋值结果&#xff08;由 always 或 initial 块赋值&#xff09;&#xff0c;通常用于建模寄存器、状态、计数器等“带记忆”的硬件行为…

使用Haporxy搭建Web群集

目录 一、案例分析 1.案例概述 2.案例前置知识点 2.1 HTTP请求 2.2 负载均衡常用调度算法 2.3常见的Web群集调度器 3.案例环境 3.1本案例环境 二、案例实施 1.搭建两台web服务器 2.安装Haproxy 3.haproxy服务器配置 修改haproxy的配置文件 4.测试web群集 5.haproxy的日…

pikachu靶场通关笔记38 目录遍历(路径遍历)

目录 一、目录遍历 二、源码分析 三、目录遍历与文件包含 四、实战渗透 1、进入靶场 2、目录遍历 &#xff08;1&#xff09;访问ace.min.css &#xff08;2&#xff09;访问fileinclude.php 本系列为《pikachu靶场通关笔记》渗透实战&#xff0c;本文通过对目录遍历源…

现代C++:std::string全方位碾压C字符串

在 C 中引入的 std::string 是对 C 语言中 char* 和 const char* 的一种现代化封装和增强。它不仅解决了 C 字符串的许多缺陷&#xff08;如安全性、内存管理、易用性等&#xff09;&#xff0c;还提供了丰富的 API 来简化字符串操作。本文将从多个维度详细对比 std::string 与…

20250619周四:Atlassian

今天主要把conference上的A xxx的所有资料大体看了一遍&#xff0c;花了两个多小时。 公司的这个conference系统&#xff0c;共实就是一个大型的可多人在线编辑的文件系统。差不多所有的资料都共享在上面。这对于多人参与的项目管理&#xff0c;还是相当方便的。 Atlassian最特…

通过CDH安装Spark的详细指南

通过CDH安装Spark的详细指南 简介 Cloudera Distribution of Hadoop (CDH) 是一个企业级的大数据平台,它集成了多个开源组件,包括Hadoop、Spark、Hive等。本文将详细介绍如何通过CDH安装和配置Spark。 前提条件 在开始安装之前,请确保满足以下条件: 已安装CDH集群具有管…

GitLab CVE-2025-5121 安全漏洞解决方案

本分分享极狐GitLab 补丁版本 18.0.2, 17.11.4, 17.10.8 的详细内容。这几个版本包含重要的缺陷和安全修复代码&#xff0c;我们强烈建议所有私有化部署用户应该立即升级到上述的某一个版本。对于极狐GitLab SaaS&#xff0c;技术团队已经进行了升级&#xff0c;无需用户采取任…

【八股消消乐】Elasticsearch优化—检索Labubu

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本专栏《八股消消乐》旨在记录个人所背的八股文&#xff0c;包括Java/Go开发、Vue开发、系统架构、大模型开发、具身智能、机器学习、深度学习、力扣算法等相关知识点&#xff…

如何实现基于场景的接口自动化测试用例?

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 自动化本身是为了提高工作效率&#xff0c;不论选择何种框架&#xff0c;何种开发语言&#xff0c;我们最终想实现的效果&#xff0c;就是让大家用最少的代码&…

FreeRTOS 任务管理学习笔记

FreeRTOS 任务管理学习笔记 引言 本文档旨在通过在STM32微控制器上使用FreeRTOS来理解和实现任务管理。实验的重点是创建和管理多个任务、处理任务同步以及通过简单的硬件接口控制任务状态。 实验概述 实验涉及创建三个任务&#xff1a; LED1_Task: 每300毫秒切换一次LED。…

c++set和pair的使用

set是C中的一种关联容器&#xff0c;具有以下特点&#xff1a; 存储唯一元素&#xff08;不允许重复&#xff09; 元素自动排序&#xff08;默认升序&#xff09; 基于红黑树实现&#xff08;平衡二叉搜索树&#xff09; 插入、删除和查找的时间复杂度为O(log n) 前言 在C…

终端命令行执行具体的方法名测试用例

你可以使用如下命令单独执行 test_mutation_login_by_email 方法:python3 manage.py test apps.login.test_client.LoginTestCase.test_mutation_login_by_email 注意事项: 路径 apps.login.test_client 要与你项目实际的 Python 包路径一致(即 test_client.py 文件所在的包…

20250620在Ubuntu20.04.6下编译KickPi的K7的Android14系统

【处理SDK】 rootrootrootroot-X99-Turbo:~/Android14$ tar zxvf rk3576-android14.0-20250217.tar.gz rootrootrootroot-X99-Turbo:~/Android14$ ll rootrootrootroot-X99-Turbo:~/Android14$ rm rk3576-android14.0-20250217.tar.gz rootrootrootroot-X99-Turbo:~/Android1…

碳中和时代的家电革命,从华为智选IAM看科技企业的环保担当

在"双碳"战略与品质消费浪潮的双重加持下&#xff0c;家电产业正经历一场前所未有的绿色革命。华为智选与空净十大品牌IAM的深度协同&#xff0c;不仅构建了智能家电领域的技术新高地&#xff0c;更通过系统性创新持续拓展着行业可持续发展的想象空间。从净水科技的突…

(C语言)Map数组的实现(数据结构)(链表)(指针)

源代码&#xff1a; #include <stdio.h> #include <stdlib.h> #include <string.h>// 键值对节点 typedef struct Node {char* key;int value;struct Node* next; } Node;// Map结构 typedef struct {Node* buckets[100]; // 固定大小的哈希桶&#xff08;…

Logback示例解析

<configuration><!-- 环境变量 --><springProperty scope"context" name"app.name" source"spring.application.name" defaultValue"application"/><!-- 日志存放路径 --><property name"log.path&qu…