Android中Ktor Client处理401/403状态码并刷新Token问题
Ktor Client 401/403 Token刷新重试问题修复
代码中的核心问题
Auth插件的Token发送范围错误
当前sendWithoutRequest仅针对www.googleapis.com发送Bearer Token,你的业务接口不会带上Token,这会导致401/403异常,同时Auth插件也无法识别需要处理的请求。HttpResponseValidator干扰Auth插件的自动处理
你在validateResponse中捕获401/403但未做正确衔接,且如果expectSuccess为true,Ktor会自动将非2xx状态转为异常,跳过Auth插件的挑战处理逻辑。refreshTokens函数返回值不严谨
链式调用ktorLoginResponse.data?.token?.let如果中间任何一步为null,会直接返回null触发登出,但没有明确的错误处理,也未更新本地保存的Token。HttpRequestRetry与Auth刷新逻辑冲突
当前Retry插件对所有非成功状态码重试,包括401/403,会在Auth插件处理刷新前就发起无效重复请求。未配置Auth插件处理403状态码
默认Bearer认证只处理401的WWW-Authenticate挑战,403不会触发刷新逻辑,需要手动添加挑战管理器。
修复后的完整代码
private fun httpClient() = HttpClient(engine = configuration.engine()) { configuration.requestRetry?.let { config -> val retryConfig: HttpRequestRetryConfig = HttpRequestRetryConfig().apply(config) install(HttpRequestRetry) { retryConfig.maxRetry?.let { maxRetries -> val max = maxRetries.invoke() // 仅在异常时重试,排除401/403交给Auth插件处理 retryOnException( maxRetries = max, retryOnTimeout = retryConfig.retryOnTimeout.invoke() ) // 重试非成功状态,但排除401/403 retryIf(maxRetries = max) { _, response -> !response.status.isSuccess() && response.status != HttpStatusCode.Unauthorized && response.status != HttpStatusCode.Forbidden } } exponentialDelay() modifyRequest { request.headers.append("x-retry-count", retryCount.toString()) } } } install(ContentNegotiation) { json( Json { ignoreUnknownKeys = true encodeDefaults = true isLenient = true prettyPrint = true explicitNulls = false }, ) } Logging { logger = object : Logger { override fun log(message: String) { if (configuration.logging) { httpMetrics.log(message) } } } level = LogLevel.ALL } install(HttpTimeout) { requestTimeoutMillis = timeoutConfig.requestTimeoutMillis connectTimeoutMillis = timeoutConfig.connectTimeoutMillis socketTimeoutMillis = timeoutConfig.socketTimeoutMillis } defaultRequest { url { protocol = URLProtocol.createOrDefault(environment.protocol) host = environment.baseUrl } contentType(configuration.contentType) } HttpResponseValidator { // 关闭自动抛出非2xx异常,让Auth插件先处理401/403 expectSuccess = false validateResponse { response -> val statusCode = response.status.value val originCall = response.call // 处理204无数据的情况 if (statusCode == 204) { throw AppException(ErrorResponse(statusCode.toString(), NO_DATA_FOUND)) } // 跳过已标记为重试的请求,避免循环验证 if (statusCode < 300 || originCall.attributes.contains(ValidateMark)) { return@validateResponse } // 除401/403外,其他错误抛出自定义异常 if (statusCode != 401 && statusCode != 403) { val exceptionCall = originCall.save().apply { attributes.put(ValidateMark, Unit) } val exceptionResponseText = try { exceptionCall.response.bodyAsText() } catch (_: MalformedInputException) { BODY_FAILED_DECODING } val errorResponse = Json.decodeFromString<ErrorResponse>(exceptionResponseText) throw AppException(errorResponse) } } } install(Auth) { bearer { // 对业务接口域名发送Token(根据实际需求调整) sendWithoutRequest { request -> request.url.host == environment.baseUrl } // 加载本地保存的Token loadTokens { val authToken = authToken() BearerTokens( accessToken = authToken.accessToken.orEmpty(), refreshToken = authToken.refreshToken.orEmpty() ) } // 添加挑战管理器,让403也触发刷新逻辑 challengeManager { challenge(BearerAuthChallengeKey) { challenge, call -> val response = call.response if (response.status == HttpStatusCode.Unauthorized || response.status == HttpStatusCode.Forbidden) { // 触发Token刷新 val newTokens = refreshTokens() if (newTokens != null) { // 重试原请求 retryWithNewTokens(newTokens) } else { // 刷新失败,执行登出 println("application will logout") // 这里添加你的登出逻辑:清空本地Token、跳转登录页等 // logout() challenge.complete() } } else { challenge.complete() } } } // 刷新Token逻辑 refreshTokens { val authToken = authToken() val workEmail = authToken.workEmail.orEmpty() val currentRefreshToken = authToken.refreshToken.orEmpty() // 调用刷新Token接口 val response = client.post { markAsRefreshTokenRequest() this.apply( RefreshToken( refreshTokenId = "", refreshToken = currentRefreshToken, email = workEmail, deviceId = deviceId!! ) ) } return@refreshTokens if (response.status == HttpStatusCode.Unauthorized || response.status == HttpStatusCode.Forbidden) { // 刷新Token失败,返回null触发登出 null } else { val ktorLoginResponse = response.body<RefreshTokenModel>() val newAccessToken = ktorLoginResponse.data?.token.orEmpty() val newRefreshToken = ktorLoginResponse.data?.refreshToken.orEmpty() if (newAccessToken.isNotEmpty() && newRefreshToken.isNotEmpty()) { // 更新本地保存的Token // saveAuthToken(authToken.copy(accessToken = newAccessToken, refreshToken = newRefreshToken)) BearerTokens(newAccessToken, newRefreshToken) } else { // Token返回不完整,视为刷新失败 null } } } } } } // 定义属性标记,避免重复验证请求 private val ValidateMark = AttributeKey<Unit>("ValidateMark")
关键修复说明
- 调整Token发送范围:
sendWithoutRequest改为匹配你的业务接口域名,确保所有需要认证的请求都带上Bearer Token。 - 分离Retry与Auth逻辑:Retry插件排除401/403,让Auth插件专门处理Token刷新重试,避免逻辑冲突。
- 配置403挑战处理:通过
challengeManager让403状态码也触发Token刷新流程。 - 完善refreshTokens逻辑:增加Token非空判断,刷新成功后更新本地Token,避免无效返回导致误登出。
- 调整HttpResponseValidator:关闭
expectSuccess,让Auth插件优先处理401/403,其他错误再抛出自定义异常。
内容的提问来源于stack exchange,提问作者Rahul Abrol
相关产品推荐
相关产品推荐

