add delta sync for dm-v21
This commit is contained in:
parent
5eb1125840
commit
177c888c84
4 changed files with 72 additions and 24 deletions
|
|
@ -22,10 +22,10 @@ data class RefreshRequest(
|
||||||
)
|
)
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RefreshResponse(
|
data class RefreshResponse(
|
||||||
val access_token: String,
|
val access_token: String? = null,
|
||||||
val expires_in: Int,
|
val expires_in: Int? = null,
|
||||||
val refresh_token: String,
|
val refresh_token: String? = null,
|
||||||
val token: String? = null // 💡 設為可空並給預設值,防止後端沒吐時崩潰
|
val token: String? = null // 💡 有些後端會直接吐 token 欄位
|
||||||
)
|
)
|
||||||
|
|
||||||
interface TecnologiaApi {
|
interface TecnologiaApi {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ object NetworkModule {
|
||||||
tokenManager.currentAccessToken.firstOrNull()
|
tokenManager.currentAccessToken.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("AuthDebug", "Interceptor - 路徑: $path, Token是否存在: ${!token.isNullOrEmpty()}")
|
||||||
|
|
||||||
val requestBuilder = originalRequest.newBuilder()
|
val requestBuilder = originalRequest.newBuilder()
|
||||||
if (!token.isNullOrEmpty()) {
|
if (!token.isNullOrEmpty()) {
|
||||||
requestBuilder.addHeader("Authorization", "Bearer $token")
|
requestBuilder.addHeader("Authorization", "Bearer $token")
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,15 @@ class TokenAuthenticator @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun authenticate(route: Route?, response: Response): Request? {
|
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 請求排隊
|
// 使用 runBlocking 配合 Mutex,讓同時間進來的 401 請求排隊
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
|
|
@ -35,13 +43,24 @@ class TokenAuthenticator @Inject constructor(
|
||||||
val currentTokenInStore = tokenManager.currentAccessToken.first()
|
val currentTokenInStore = tokenManager.currentAccessToken.first()
|
||||||
val oldRequestToken = response.request.header("Authorization")?.removePrefix("Bearer ")
|
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 不同了
|
// 🌟 防禦機制:如果 DataStore 裡的 Token 已經跟當初發起請求時的舊 Token 不同了
|
||||||
// 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。
|
// 代表排在你前面的請求已經幫忙刷新成功了!這裡直接拿新 Token 重試即可,不用再打一次 API。
|
||||||
if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) {
|
if (currentTokenInStore != oldRequestToken && !currentTokenInStore.isNullOrEmpty()) {
|
||||||
Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token")
|
Log.d("AuthDebug", "分流防禦:偵測到其他請求已刷新過 Token,直接重複利用新 Token")
|
||||||
return@runBlocking response.request.newBuilder()
|
val requestBuilder = response.request.newBuilder()
|
||||||
.header("Authorization", "Bearer $currentTokenInStore")
|
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()
|
.build()
|
||||||
|
requestBuilder.url(newUrl)
|
||||||
|
}
|
||||||
|
return@runBlocking requestBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 真正準備去打後端刷新
|
// 2. 真正準備去打後端刷新
|
||||||
|
|
@ -54,33 +73,58 @@ class TokenAuthenticator @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 同步發送網路請求
|
// 3. 同步發送網路請求
|
||||||
|
Log.d("AuthDebug", "正在向伺服器請求刷新 Token... RefreshToken: ${refreshToken.take(10)}...")
|
||||||
val refreshResponse = try {
|
val refreshResponse = try {
|
||||||
apiService.get().refreshToken(RefreshRequest(refreshToken))
|
apiService.get().refreshToken(RefreshRequest(refreshToken))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}")
|
Log.e("AuthDebug", "打刷新 API 發生網路異常: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 判斷刷新是否成功
|
// 4. 判斷刷新是否成功
|
||||||
if (refreshResponse != null && refreshResponse.isSuccessful) {
|
if (refreshResponse != null) {
|
||||||
|
Log.d("AuthDebug", "刷新回應碼: ${refreshResponse.code()}, 是否成功: ${refreshResponse.isSuccessful}")
|
||||||
|
if (refreshResponse.isSuccessful) {
|
||||||
val newAuthData = refreshResponse.body()
|
val newAuthData = refreshResponse.body()
|
||||||
if (newAuthData != null) {
|
val newAccessToken = newAuthData?.access_token ?: newAuthData?.token
|
||||||
Log.d("AuthDebug", "✅ Token 刷新成功,準備寫入 DataStore")
|
|
||||||
|
if (!newAccessToken.isNullOrEmpty()) {
|
||||||
|
Log.d("AuthDebug", "✅ Token 刷新成功,新 AccessToken: ${newAccessToken.take(10)}...")
|
||||||
|
|
||||||
|
// 如果後端沒給新的 refresh_token,就沿用舊的
|
||||||
|
val nextRefreshToken = if (!newAuthData?.refresh_token.isNullOrEmpty()) {
|
||||||
|
newAuthData?.refresh_token!!
|
||||||
|
} else {
|
||||||
|
refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
// 導正順序:確實執行完 saveAuthData
|
|
||||||
tokenManager.saveAuthData(
|
tokenManager.saveAuthData(
|
||||||
userId = currentUserId,
|
userId = currentUserId,
|
||||||
accessToken = newAuthData.access_token,
|
accessToken = newAccessToken,
|
||||||
refreshToken = newAuthData.refresh_token,
|
refreshToken = nextRefreshToken,
|
||||||
expiresInSec = newAuthData.expires_in
|
expiresInSec = newAuthData?.expires_in ?: 3600
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求")
|
Log.d("AuthDebug", "✅ DataStore 寫入完成,重新發送原請求")
|
||||||
|
|
||||||
return@runBlocking response.request.newBuilder()
|
// 修正:同步更新 Header 與 URL 中的 token 參數 (針對 WebSocket)
|
||||||
.header("Authorization", "Bearer ${newAuthData.access_token}")
|
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()
|
.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 都失效,強制登出
|
// 5. 失敗:連 Refresh Token 都失效,強制登出
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,14 @@ class TokenManager @Inject constructor(
|
||||||
|
|
||||||
// 儲存資料(維持原樣,這是安全的)
|
// 儲存資料(維持原樣,這是安全的)
|
||||||
suspend fun saveAuthData(userId: String, accessToken: String, refreshToken: String, expiresInSec: Int) {
|
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 ->
|
dataStore.edit { preferences ->
|
||||||
preferences[accessTokenKey(userId)] = accessToken
|
preferences[accessTokenKey(userId)] = accessToken
|
||||||
preferences[refreshTokenKey(userId)] = refreshToken
|
preferences[refreshTokenKey(userId)] = refreshToken
|
||||||
|
|
||||||
val expiresAt = System.currentTimeMillis() + (expiresInSec * 1000L)
|
val expiresAt = System.currentTimeMillis() + (expiresInSec * 1000L)
|
||||||
preferences[expiresAtKey(userId)] = expiresAt
|
preferences[expiresAtKey(userId)] = expiresAt
|
||||||
|
android.util.Log.d("TokenManager", "Token 將於 ${(expiresInSec / 60)} 分鐘後過期 (timestamp: $expiresAt)")
|
||||||
|
|
||||||
preferences[CURRENT_USER_ID] = userId
|
preferences[CURRENT_USER_ID] = userId
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue