From 177c888c849073e8df34deb03db9e2a0b7cf5b85 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 9 Aug 2026 19:56:10 +0800 Subject: [PATCH] add delta sync for dm-v21 --- .../login_v3/data/api/Main_API_Service.kt | 8 +- .../example/login_v3/data/di/NetworkModule.kt | 2 + .../data/repository/basic/Token_Auth.kt | 84 ++++++++++++++----- .../data/repository/basic/Token_Manager.kt | 2 + 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt index f359fdf..4326128 100644 --- a/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt +++ b/app/src/main/java/com/example/login_v3/data/api/Main_API_Service.kt @@ -22,10 +22,10 @@ data class RefreshRequest( ) @JsonClass(generateAdapter = true) data class RefreshResponse( - val access_token: String, - val expires_in: Int, - val refresh_token: String, - val token: String? = null // 💡 設為可空並給預設值,防止後端沒吐時崩潰 + val access_token: String? = null, + val expires_in: Int? = null, + val refresh_token: String? = null, + val token: String? = null // 💡 有些後端會直接吐 token 欄位 ) interface TecnologiaApi { diff --git a/app/src/main/java/com/example/login_v3/data/di/NetworkModule.kt b/app/src/main/java/com/example/login_v3/data/di/NetworkModule.kt index 59911ab..3ace8a6 100644 --- a/app/src/main/java/com/example/login_v3/data/di/NetworkModule.kt +++ b/app/src/main/java/com/example/login_v3/data/di/NetworkModule.kt @@ -56,6 +56,8 @@ object NetworkModule { tokenManager.currentAccessToken.firstOrNull() } + android.util.Log.d("AuthDebug", "Interceptor - 路徑: $path, Token是否存在: ${!token.isNullOrEmpty()}") + val requestBuilder = originalRequest.newBuilder() if (!token.isNullOrEmpty()) { requestBuilder.addHeader("Authorization", "Bearer $token") diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt index 3dc6a02..85603cc 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Auth.kt @@ -25,7 +25,15 @@ class TokenAuthenticator @Inject constructor( } override fun authenticate(route: Route?, response: Response): Request? { - Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: ${response.request.url}) ---") + val url = response.request.url + Log.d("AuthDebug", "--- 進入 Authenticator (觸發網址: $url) ---") + + // 🌟 防禦機制:如果刷新 Token 的請求本身又回傳了 401,代表 Refresh Token 已徹底過期 + // 必須直接回傳 null,否則會進入無窮迴圈(甚至死鎖,因為 Mutex 不可重入) + if (url.encodedPath.contains("api/auth/refresh")) { + Log.e("AuthDebug", "❌ 刷新 Token 的請求本身又回傳了 401!停止重試。") + return null + } // 使用 runBlocking 配合 Mutex,讓同時間進來的 401 請求排隊 return runBlocking { @@ -35,13 +43,24 @@ class TokenAuthenticator @Inject constructor( val currentTokenInStore = tokenManager.currentAccessToken.first() val oldRequestToken = response.request.header("Authorization")?.removePrefix("Bearer ") + Log.d("AuthDebug", "DataStore中的Token: ${currentTokenInStore?.take(10)}...") + Log.d("AuthDebug", "原始請求的Token: ${oldRequestToken?.take(10)}...") + // 🌟 防禦機制:如果 DataStore 裡的 Token 已經跟當初發起請求時的舊 Token 不同了 // 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。 if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) { Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token") - return@runBlocking response.request.newBuilder() - .header("Authorization", "Bearer $currentTokenInStore") - .build() + val requestBuilder = response.request.newBuilder() + requestBuilder.header("Authorization", "Bearer $currentTokenInStore") + + // 針對 WebSocket 更新 URL 中的 token 參數 + if (response.request.url.queryParameter("token") != null) { + val newUrl = response.request.url.newBuilder() + .setQueryParameter("token", currentTokenInStore) + .build() + requestBuilder.url(newUrl) + } + return@runBlocking requestBuilder.build() } // 2. 真正準備去打後端刷新 @@ -54,33 +73,58 @@ class TokenAuthenticator @Inject constructor( } // 3. 同步發送網路請求 + Log.d("AuthDebug", "正在向伺服器請求刷新 Token... RefreshToken: ${refreshToken.take(10)}...") val refreshResponse = try { apiService.get().refreshToken(RefreshRequest(refreshToken)) } catch (e: Exception) { - Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}") + Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}", e) null } // 4. 判斷刷新是否成功 - if (refreshResponse != null && refreshResponse.isSuccessful) { - val newAuthData = refreshResponse.body() - if (newAuthData != null) { - Log.d("AuthDebug", "✅ Token 刷新成功,準備寫入 DataStore") + if (refreshResponse != null) { + Log.d("AuthDebug", "刷新回應碼: ${refreshResponse.code()}, 是否成功: ${refreshResponse.isSuccessful}") + if (refreshResponse.isSuccessful) { + val newAuthData = refreshResponse.body() + val newAccessToken = newAuthData?.access_token ?: newAuthData?.token - // 導正順序:確實執行完 saveAuthData - tokenManager.saveAuthData( - userId = currentUserId, - accessToken = newAuthData.access_token, - refreshToken = newAuthData.refresh_token, - expiresInSec = newAuthData.expires_in - ) + if (!newAccessToken.isNullOrEmpty()) { + Log.d("AuthDebug", "✅ Token 刷新成功,新 AccessToken: ${newAccessToken.take(10)}...") - Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求") + // 如果後端沒給新的 refresh_token,就沿用舊的 + val nextRefreshToken = if (!newAuthData?.refresh_token.isNullOrEmpty()) { + newAuthData?.refresh_token!! + } else { + refreshToken + } - return@runBlocking response.request.newBuilder() - .header("Authorization", "Bearer ${newAuthData.access_token}") - .build() + tokenManager.saveAuthData( + userId = currentUserId, + accessToken = newAccessToken, + refreshToken = nextRefreshToken, + expiresInSec = newAuthData?.expires_in ?: 3600 + ) + + Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求") + + // 修正:同步更新 Header 與 URL 中的 token 參數 (針對 WebSocket) + val requestBuilder = response.request.newBuilder() + requestBuilder.header("Authorization", "Bearer $newAccessToken") + if (response.request.url.queryParameter("token") != null) { + val newUrl = response.request.url.newBuilder() + .setQueryParameter("token", newAccessToken) + .build() + requestBuilder.url(newUrl) + } + return@runBlocking requestBuilder.build() + } else { + Log.e("AuthDebug", "❌ 刷新成功但無法取得新 Token (Body: $newAuthData)") + } + } else { + Log.e("AuthDebug", "❌ 刷新失敗,錯誤代碼: ${refreshResponse.code()}, 錯誤內容: ${refreshResponse.errorBody()?.string()}") } + } else { + Log.e("AuthDebug", "❌ refreshResponse 為 null (網路異常)") } // 5. 失敗:連 Refresh Token 都失效,強制登出 diff --git a/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Manager.kt b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Manager.kt index b1caff6..ce04257 100644 --- a/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Manager.kt +++ b/app/src/main/java/com/example/login_v3/data/repository/basic/Token_Manager.kt @@ -31,12 +31,14 @@ class TokenManager @Inject constructor( // 儲存資料(維持原樣,這是安全的) suspend fun saveAuthData(userId: String, accessToken: String, refreshToken: String, expiresInSec: Int) { + android.util.Log.d("TokenManager", "儲存 Token - User: $userId, AccessToken: ${accessToken.take(10)}..., RefreshToken: ${refreshToken.take(10)}...") dataStore.edit { preferences -> preferences[accessTokenKey(userId)] = accessToken preferences[refreshTokenKey(userId)] = refreshToken val expiresAt = System.currentTimeMillis() + (expiresInSec * 1000L) preferences[expiresAtKey(userId)] = expiresAt + android.util.Log.d("TokenManager", "Token 將於 ${(expiresInSec / 60)} 分鐘後過期 (timestamp: $expiresAt)") preferences[CURRENT_USER_ID] = userId