服务端一直签 access(30 分钟)和 refresh(14 天)两个令牌,但两个客户端拿到 401 都是直接清 令牌跳登录页,refresh 从来没被用过——"14 天有效"只是纸面参数,真实寿命就是 30 分钟。 - web/client.ts:请求前剩不到 90 秒先换;撞 401 先换再重放一次原请求;并发共用一次刷新; 上传附件同一条路 - android/Api.kt:call 拆 call/callOnce,2002 时用 Mutex 串行刷新后重放 - 后端 /auth/refresh 看一眼账号状态:停用的账号不能靠 refresh 一直续(2012) - 新增 session-refresh.mjs:假 access + 真 refresh 要能留住人,两个都假才回登录页
This commit is contained in:
@@ -2,6 +2,8 @@ package com.dadao.task.data
|
||||
|
||||
import com.dadao.task.BuildConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -789,11 +791,55 @@ class Api(private val tokens: TokenStore) {
|
||||
method = "PATCH",
|
||||
)
|
||||
|
||||
/**
|
||||
* 访问令牌只有 30 分钟,refresh 有 14 天。
|
||||
*
|
||||
* 撞上 2002(登录已过期)时先用 refresh 换一对新令牌,把这次请求原样重放一次;
|
||||
* 换不动才清令牌让界面回登录页。以前是直接清,用户用一会儿就被踢出去。
|
||||
* 并发请求共用一把锁:刷新只发一次,其余等它换完直接用新令牌。
|
||||
*/
|
||||
private suspend inline fun <reified T> call(
|
||||
path: String,
|
||||
body: String? = null,
|
||||
auth: Boolean = false,
|
||||
method: String? = null,
|
||||
): T {
|
||||
val stale = tokens.accessToken
|
||||
return try {
|
||||
callOnce<T>(path, body, auth, method)
|
||||
} catch (e: ApiException) {
|
||||
if (!auth || e.code != 2002 || !refreshTokens(stale)) throw e
|
||||
callOnce<T>(path, body, auth, method)
|
||||
}
|
||||
}
|
||||
|
||||
private val refreshLock = Mutex()
|
||||
|
||||
private suspend fun refreshTokens(stale: String?): Boolean {
|
||||
val refresh = tokens.refreshToken ?: return false
|
||||
return refreshLock.withLock {
|
||||
// 等锁期间别人可能已经换完了:本地令牌已经不是失败的那一个,直接用新的
|
||||
val current = tokens.accessToken
|
||||
if (current != null && current != stale) return@withLock true
|
||||
try {
|
||||
val fresh: Tokens = call(
|
||||
path = "/auth/refresh",
|
||||
body = json.encodeToString(RefreshRequest(refresh)),
|
||||
)
|
||||
tokens.save(fresh.accessToken, fresh.refreshToken)
|
||||
true
|
||||
} catch (e: ApiException) {
|
||||
tokens.clear()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> callOnce(
|
||||
path: String,
|
||||
body: String? = null,
|
||||
auth: Boolean = false,
|
||||
method: String? = null,
|
||||
): T = withContext(Dispatchers.IO) {
|
||||
val builder = Request.Builder().url(base + path)
|
||||
val verb = method ?: if (body == null) "GET" else "POST"
|
||||
@@ -817,7 +863,6 @@ class Api(private val tokens: TokenStore) {
|
||||
throw ApiException(1500, "响应格式异常(HTTP ${response.code})")
|
||||
}
|
||||
if (envelope.code != 0) {
|
||||
if (envelope.code == 2002 && auth) tokens.clear()
|
||||
throw ApiException(envelope.code, envelope.message, envelope.requestId)
|
||||
}
|
||||
envelope.data ?: throw ApiException(1500, "响应缺少 data")
|
||||
|
||||
Reference in New Issue
Block a user