This commit is contained in:
Eric85710 2026-07-01 17:24:06 +08:00
parent c3c861ac62
commit 033bea2a4e

View file

@ -6,69 +6,88 @@ import com.example.login_v3.data.api.TecnologiaApi
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator import okhttp3.Authenticator
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Request import okhttp3.Request
import okhttp3.Route
import okhttp3.Response import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject import javax.inject.Inject
class TokenAuthenticator @Inject constructor( class TokenAuthenticator @Inject constructor(
private val tokenManager: TokenManager, private val tokenManager: TokenManager,
// 💡 關鍵:使用 dagger.Lazy 避免 Hilt 的循環依賴 (Circular Dependency)
private val apiService: dagger.Lazy<TecnologiaApi> private val apiService: dagger.Lazy<TecnologiaApi>
) : Authenticator { ) : Authenticator {
companion object {
// 🌟 關鍵:全域共用同一把鎖,防止多個 API 同時過期時重複打刷新介面
private val refreshMutex = Mutex()
}
override fun authenticate(route: Route?, response: Response): Request? { override fun authenticate(route: Route?, response: Response): Request? {
// 1. 取得 DataStore 中的當前帳號資訊
Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: ${response.request.url}) ---") Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: ${response.request.url}) ---")
val refreshToken = runBlocking { tokenManager.currentRefreshToken.first() }
val currentUserId = runBlocking { tokenManager.currentUserId.first() }
Log.d("AuthDebug", "當前用戶: $currentUserId, Token是否存在: ${!refreshToken.isNullOrEmpty()}")
if (refreshToken.isNullOrEmpty() || currentUserId.isNullOrEmpty()) { // 使用 runBlocking 配合 Mutex,讓同時間進來的 401 請求排隊
return null // 沒有憑證資訊,不重試,直接拋出原 401 錯誤 return runBlocking {
} refreshMutex.withLock {
// 2. 同步發送網路請求,拿 refresh_token 去換新 Token // 1. 檢查目前 DataStore 裡的 Access Token
val refreshResponse = runBlocking { val currentTokenInStore = tokenManager.currentAccessToken.first()
try { val oldRequestToken = response.request.header("Authorization")?.removePrefix("Bearer ")
// 用 apiService.get() 來取得真正需要用的實例
apiService.get().refreshToken(RefreshRequest(refreshToken))
} catch (e: Exception) {
null
}
}
// 3. 判斷刷新是否成功 // 🌟 防禦機制:如果 DataStore 裡的 Token 已經跟當初發起請求時的舊 Token 不同了
if (refreshResponse != null && refreshResponse.isSuccessful) { // 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。
Log.d("AuthDebug", "✅ Token 刷新成功,準備寫入 DataStore") if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) {
// saveAuthData... Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token")
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求") return@runBlocking response.request.newBuilder()
val newAuthData = refreshResponse.body() .header("Authorization", "Bearer $currentTokenInStore")
if (newAuthData != null) { .build()
// 4. 成功:更新本地 DataStore 資料
runBlocking {
tokenManager.saveAuthData(
userId = currentUserId,
accessToken = newAuthData.access_token,
refreshToken = newAuthData.refresh_token,
expiresInSec = newAuthData.expires_in
)
} }
// 5. 成功:用新的 Access Token 重新建構剛才失敗的 API 請求 // 2. 真正準備去打後端刷新
return response.request.newBuilder() val refreshToken = tokenManager.currentRefreshToken.first()
.header("Authorization", "Bearer ${newAuthData.access_token}") val currentUserId = tokenManager.currentUserId.first()
.build() Log.d("AuthDebug", "當前用戶: $currentUserId, Token是否存在: ${!refreshToken.isNullOrEmpty()}")
if (refreshToken.isNullOrEmpty() || currentUserId.isNullOrEmpty()) {
return@runBlocking null
}
// 3. 同步發送網路請求
val refreshResponse = try {
apiService.get().refreshToken(RefreshRequest(refreshToken))
} catch (e: Exception) {
Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}")
null
}
// 4. 判斷刷新是否成功
if (refreshResponse != null && refreshResponse.isSuccessful) {
val newAuthData = refreshResponse.body()
if (newAuthData != null) {
Log.d("AuthDebug", "✅ Token 刷新成功,準備寫入 DataStore")
// 導正順序:確實執行完 saveAuthData
tokenManager.saveAuthData(
userId = currentUserId,
accessToken = newAuthData.access_token,
refreshToken = newAuthData.refresh_token,
expiresInSec = newAuthData.expires_in
)
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求")
return@runBlocking response.request.newBuilder()
.header("Authorization", "Bearer ${newAuthData.access_token}")
.build()
}
}
// 5. 失敗:連 Refresh Token 都失效,強制登出
Log.e("AuthDebug", "❌ Refresh Token 已失效,執行強制登出!")
tokenManager.logout(currentUserId)
return@runBlocking null
} }
} }
// 6. 失敗:如果連 Refresh Token 都失效(例如過期被後端拒絕),則強制登出,導回登入頁
// TokenAuthenticator.kt 失敗時
runBlocking {
tokenManager.logout(currentUserId) // 這會移除 USER_ID_LIST,觸發 hasAccountLoggedIn = false
}
return null
} }
} }