dm-187
This commit is contained in:
parent
c3c861ac62
commit
033bea2a4e
1 changed files with 65 additions and 46 deletions
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue