Kotlin Multiplatform Mobile中基于Ktor实现401状态下的RefreshToken刷新及重试流程指导
KMM + Ktor 实现401自动刷新Token并重试请求
嘿,我来帮你搞定这个KMM项目里Ktor的401自动刷新token逻辑!这确实是跨平台认证里很常见的场景,咱们结合你现有的代码一步步完善,确保逻辑完整且流畅。
核心实现思路
咱们要利用Ktor的Auth插件自带的bearer认证机制,它内置了token刷新和请求重试的逻辑,不用手动去捕获401再重试。核心流程是:
- 每次请求自动携带当前的AccessToken
- 当请求返回401时,自动调用RefreshToken接口获取新的Token
- 成功获取新Token后,Ktor会自动重试原请求
- 如果RefreshToken接口也返回401(或者其他失败情况),触发应用登出操作
完整代码实现
第一步:CommonMain 定义跨平台接口
先在common模块里定义通用的Token管理和登出回调接口,让Android和iOS端各自实现:
// commonMain/.../AuthCommon.kt expect interface TokenProvider { var accessToken: String var refreshToken: String } expect interface LogoutCallback { fun onLogout() } expect fun httpClient( tokenProvider: TokenProvider, logoutCallback: LogoutCallback, config: HttpClientConfig<*>.() -> Unit = {} ): HttpClient // 定义Token响应实体,放在commonMain里 data class TokenResponse( val access_token: String, val refresh_token: String )
第二步:Android端实现
更新你的AndroidMain代码和Platform相关类,完善HttpClient配置、Token持久化和登出逻辑:
AndroidMain HttpClient配置
// androidMain/.../HttpClient.kt actual fun httpClient( tokenProvider: TokenProvider, logoutCallback: LogoutCallback, config: HttpClientConfig<*>.() -> Unit ) = HttpClient(OkHttp) { config(this) // 序列化配置 install(ContentNegotiation) { json(Json { prettyPrint = true ignoreUnknownKeys = true explicitNulls = false }) } // OkHttp引擎配置 engine { config { retryOnConnectionFailure(true) connectTimeout(30, TimeUnit.SECONDS) readTimeout(40, TimeUnit.SECONDS) } } // 全局请求头 defaultRequest { header("Client-Version", Platform().versionCode) } // Bearer认证核心配置 install(Auth) { bearer { // 加载当前存储的Token loadTokens { BearerTokens( accessToken = tokenProvider.accessToken, refreshToken = tokenProvider.refreshToken ) } // Token刷新逻辑 refreshTokens { try { // 调用你的RefreshToken接口,替换成实际的API地址 val newTokens = client.post("https://your-api-domain.com/auth/refresh") { setBody(mapOf("refresh_token" to oldTokens.refreshToken)) contentType(ContentType.Application.Json) }.body<TokenResponse>() // 更新本地Token存储 tokenProvider.accessToken = newTokens.access_token tokenProvider.refreshToken = newTokens.refresh_token (tokenProvider as AndroidTokenProvider).saveTokens(newTokens.access_token, newTokens.refresh_token) // 返回新Token,Ktor自动重试原请求 BearerTokens(newTokens.access_token, newTokens.refresh_token) } catch (e: Exception) { // 刷新失败(包括401、网络错误等),触发登出 logoutCallback.onLogout() throw e // 终止重试流程 } } } } // 额外的响应校验,处理刷新后仍返回401的情况 HttpResponseValidator { validateResponse { response -> if (response.status.value == 401) { logoutCallback.onLogout() } } } }
Android端TokenProvider和LogoutCallback实现
// androidMain/.../AuthImpl.kt import android.content.Context import android.content.Intent import android.content.SharedPreferences import com.example.yourpackage.LoginActivity // 替换成你的登录页 class AndroidTokenProvider(private val context: Context) : TokenProvider { override var accessToken: String = "" override var refreshToken: String = "" private val sharedPref: SharedPreferences = context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE) init { // 初始化时从SharedPreferences读取Token accessToken = sharedPref.getString("access_token", "") ?: "" refreshToken = sharedPref.getString("refresh_token", "") ?: "" } // 持久化保存Token fun saveTokens(accessToken: String, refreshToken: String) { this.accessToken = accessToken this.refreshToken = refreshToken sharedPref.edit() .putString("access_token", accessToken) .putString("refresh_token", refreshToken) .apply() } } class AndroidLogoutCallback(private val context: Context) : LogoutCallback { override fun onLogout() { // 执行登出操作:清空Token、跳转登录页 val tokenProvider = AndroidTokenProvider(context) tokenProvider.saveTokens("", "") val intent = Intent(context, LoginActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) } }
更新Android端Platform.kt
// androidMain/.../Platform.kt lateinit var provider: VersionAndroidProvider lateinit var tokenProvider: AndroidTokenProvider lateinit var logoutCallback: AndroidLogoutCallback actual class Platform actual constructor() { actual val versionCode get() = provider.version actual val accessToken: String get() = tokenProvider.accessToken } interface VersionAndroidProvider { val version: String }
第三步:iOS端实现
同理,完善iOS端的HttpClient、Token管理和登出逻辑:
iOSMain HttpClient配置
// iosMain/.../HttpClient.kt import platform.Foundation.NSBundle import platform.UIKit.UIApplication import platform.UIKit.UIViewController actual fun httpClient( tokenProvider: TokenProvider, logoutCallback: LogoutCallback, config: HttpClientConfig<*>.() -> Unit ) = HttpClient(Ios) { config(this) install(ContentNegotiation) { json(Json { prettyPrint = true ignoreUnknownKeys = true explicitNulls = false }) } engine { configureRequest { timeoutIntervalForRequest = 40.0 timeoutIntervalForResource = 30.0 } } defaultRequest { header("Client-Version", Platform().versionCode) } install(Auth) { bearer { loadTokens { BearerTokens(tokenProvider.accessToken, tokenProvider.refreshToken) } refreshTokens { try { val newTokens = client.post("https://your-api-domain.com/auth/refresh") { setBody(mapOf("refresh_token" to oldTokens.refreshToken)) contentType(ContentType.Application.Json) }.body<TokenResponse>() tokenProvider.accessToken = newTokens.access_token tokenProvider.refreshToken = newTokens.refresh_token (tokenProvider as IOSTokenProvider).saveTokens(newTokens.access_token, newTokens.refresh_token) BearerTokens(newTokens.access_token, newTokens.refresh_token) } catch (e: Exception) { logoutCallback.onLogout() throw e } } } } HttpResponseValidator { validateResponse { response -> if (response.status.value == 401) { logoutCallback.onLogout() } } } }
iOS端TokenProvider和LogoutCallback实现
// iosMain/.../AuthImpl.kt import platform.Foundation.NSUserDefaults import platform.UIKit.UIApplication import platform.UIKit.UIViewController class IOSTokenProvider : TokenProvider { override var accessToken: String = "" override var refreshToken: String = "" private val defaults = NSUserDefaults.standardUserDefaults init { accessToken = defaults.stringForKey("access_token") ?: "" refreshToken = defaults.stringForKey("refresh_token") ?: "" } fun saveTokens(accessToken: String, refreshToken: String) { this.accessToken = accessToken this.refreshToken = refreshToken defaults.setObject(accessToken, forKey = "access_token") defaults.setObject(refreshToken, forKey = "refresh_token") defaults.synchronize() } } class IOSLogoutCallback : LogoutCallback { override fun onLogout() { // 清空Token并跳转登录页 val tokenProvider = IOSTokenProvider() tokenProvider.saveTokens("", "") val window = UIApplication.sharedApplication.keyWindow window?.rootViewController = LoginViewController() // 替换成你的iOS登录页 } }
更新iOS端Platform.kt
// iosMain/.../Platform.kt lateinit var tokenProvider: IOSTokenProvider actual class Platform actual constructor() { actual val versionCode = NSBundle.mainBundle.infoDictionary?.objectForKey("CFBundleVersion") as? String ?: "" actual val accessToken: String get() = tokenProvider.accessToken }
关键技术点说明
- Ktor Bearer插件的优势:
- 自动处理401触发token刷新,无需手动拦截请求
- 刷新成功后自动重试原请求,省去手动重新发起请求的代码
- Token持久化:
- Android用
SharedPreferences,iOS用NSUserDefaults,如果需要更高安全性,可以改用Android的EncryptedSharedPreferences或iOS的Keychain
- Android用
- 异常处理:
- 在
refreshTokens里捕获所有异常(网络错误、401等),只要刷新失败就触发登出,避免无限重试
- 在
- 线程安全:
TokenProvider的读写操作要注意线程安全,SharedPreferences和NSUserDefaults本身是线程安全的,但如果用其他存储方式需要额外处理
内容的提问来源于stack exchange,提问作者Vivek Modi
相关产品推荐
相关产品推荐

