You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Multiplatform Mobile中基于Ktor实现401状态下的RefreshToken刷新及重试流程指导

KMM + Ktor 实现401自动刷新Token并重试请求

嘿,我来帮你搞定这个KMM项目里Ktor的401自动刷新token逻辑!这确实是跨平台认证里很常见的场景,咱们结合你现有的代码一步步完善,确保逻辑完整且流畅。

核心实现思路

咱们要利用Ktor的Auth插件自带的bearer认证机制,它内置了token刷新和请求重试的逻辑,不用手动去捕获401再重试。核心流程是:

  1. 每次请求自动携带当前的AccessToken
  2. 当请求返回401时,自动调用RefreshToken接口获取新的Token
  3. 成功获取新Token后,Ktor会自动重试原请求
  4. 如果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
}

关键技术点说明

  1. Ktor Bearer插件的优势:
    • 自动处理401触发token刷新,无需手动拦截请求
    • 刷新成功后自动重试原请求,省去手动重新发起请求的代码
  2. Token持久化:
    • Android用SharedPreferences,iOS用NSUserDefaults,如果需要更高安全性,可以改用Android的EncryptedSharedPreferences或iOS的Keychain
  3. 异常处理:
    • 在refreshTokens里捕获所有异常(网络错误、401等),只要刷新失败就触发登出,避免无限重试
  4. 线程安全:
    • TokenProvider的读写操作要注意线程安全,SharedPreferences和NSUserDefaults本身是线程安全的,但如果用其他存储方式需要额外处理

内容的提问来源于stack exchange,提问作者Vivek Modi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 18:47:28