-
Notifications
You must be signed in to change notification settings - Fork 3
[FIX] 토큰 재발급 및 실패시 로직 수정 #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
77213bc
bdcc060
c903781
bb70a3d
320e624
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.kuit.ourmenu.data.service | ||
|
|
||
| import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse | ||
| import com.kuit.ourmenu.data.model.base.BaseResponse | ||
| import retrofit2.http.Body | ||
| import retrofit2.http.POST | ||
|
|
||
| interface TokenService { | ||
| @POST("api/users/reissue-token") | ||
| suspend fun reissueToken( | ||
| @Body refreshToken: String | ||
| ): BaseResponse<ReissueTokenResponse> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| package com.kuit.ourmenu.utils.auth | ||
|
|
||
| import android.util.Log | ||
| import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory | ||
| import com.kuit.ourmenu.BuildConfig | ||
| import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse | ||
| import com.kuit.ourmenu.data.service.AuthService | ||
| import com.kuit.ourmenu.data.service.TokenService | ||
| import kotlinx.coroutines.flow.first | ||
| import kotlinx.coroutines.runBlocking | ||
| import kotlinx.serialization.json.Json | ||
|
|
@@ -13,50 +13,53 @@ import okhttp3.OkHttpClient | |
| import okhttp3.Request | ||
| import okhttp3.Response | ||
| import okhttp3.Route | ||
| import okhttp3.logging.HttpLoggingInterceptor | ||
| import retrofit2.Retrofit | ||
| import javax.inject.Inject | ||
|
|
||
| class TokenAuthenticator @Inject constructor( | ||
| private val tokenManager: TokenManager, | ||
| ): Authenticator { | ||
| override fun authenticate(route: Route?, response: Response): Request? { | ||
| val refreshToken = runBlocking { | ||
| tokenManager.getRefreshToken().first() | ||
| // 이미 재시도했던 요청인 경우 null을 반환하여 재시도 중단 | ||
| if (response.request.header("Retry-With-New-Token") != null) { | ||
| return null | ||
| } | ||
|
|
||
| return runBlocking { | ||
| if (refreshToken.isNullOrEmpty()){ | ||
| return@runBlocking null | ||
| } | ||
| val newToken = getNewToken(refreshToken) | ||
| try { | ||
| val refreshToken = tokenManager.getRefreshToken().first() | ||
| if (refreshToken == null) { | ||
| tokenManager.clearToken() | ||
| return@runBlocking null | ||
| } | ||
|
|
||
| if (newToken == null) { | ||
| tokenManager.clearToken() | ||
| } | ||
| // 토큰 재발급 요청 | ||
| val tokenService = Retrofit.Builder() | ||
| .baseUrl(BuildConfig.BASE_URL) | ||
| .client(OkHttpClient.Builder().build()) | ||
| .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) | ||
| .build() | ||
| .create(TokenService::class.java) | ||
| val tokenResponse = tokenService.reissueToken( | ||
| refreshToken = refreshToken | ||
| ) | ||
|
|
||
|
Comment on lines
+43
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reissueToken 호출 시 String 바디 사용 문제 (실패 원인) TokenService가 요청 DTO를 받도록 바뀌면 여기 호출도 DTO로 교체해야 합니다. 적용 diff: +import com.kuit.ourmenu.data.model.auth.request.ReissueTokenRequest
@@
- val tokenResponse = tokenService.reissueToken(
- refreshToken = refreshToken
- )
+ val tokenResponse = tokenService.reissueToken(
+ body = ReissueTokenRequest(refreshToken = refreshToken)
+ )Also applies to: 3-7 🤖 Prompt for AI Agents |
||
| // 새로운 토큰 저장 | ||
| tokenManager.saveAccessToken(tokenResponse.response?.accessToken ?: "") | ||
| tokenManager.saveRefreshToken(tokenResponse.response?.refreshToken ?: "") | ||
| Log.d("TokenAuthenticator", "토큰 재발급 성공: ${tokenResponse.response?.accessToken}") | ||
|
|
||
| newToken?.let { | ||
| tokenManager.saveAccessToken(it.accessToken) | ||
| // 기존 요청에 새로운 토큰으로 헤더를 추가하여 재시도 | ||
| response.request.newBuilder() | ||
| .header("Authorization", "Bearer ${it.accessToken}") | ||
| .header("Authorization", "Bearer ${tokenResponse.response?.accessToken}") | ||
| .header("Retry-With-New-Token", "true") | ||
| .build() | ||
|
Comment on lines
+48
to
56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 널/빈 토큰 저장 및 ‘Bearer null’ 헤더 위험 + 민감정보 로그 노출
적용 diff: @@
- // 새로운 토큰 저장
- tokenManager.saveAccessToken(tokenResponse.response?.accessToken ?: "")
- tokenManager.saveRefreshToken(tokenResponse.response?.refreshToken ?: "")
- Log.d("TokenAuthenticator", "토큰 재발급 성공: ${tokenResponse.response?.accessToken}")
+ // 새로운 토큰 저장 (검증)
+ val newAccess = tokenResponse.response?.accessToken?.takeIf { it.isNotBlank() }
+ val newRefresh = tokenResponse.response?.refreshToken?.takeIf { it.isNotBlank() }
+ if (newAccess == null || newRefresh == null) {
+ Log.w("TokenAuthenticator", "토큰 재발급 응답에 토큰이 없습니다.")
+ tokenManager.clearToken()
+ return@runBlocking null
+ }
+ tokenManager.saveAccessToken(newAccess)
+ tokenManager.saveRefreshToken(newRefresh)
+ Log.d("TokenAuthenticator", "토큰 재발급 성공") // 민감정보 비노출
@@
- .header("Authorization", "Bearer ${tokenResponse.response?.accessToken}")
+ .header("Authorization", "Bearer $newAccess")
.header("Retry-With-New-Token", "true")
.build()
@@
- // 토큰 재발급 실패 시 토큰 클리어
- Log.d("TokenAuthenticator", "토큰 재발급 실패: ${e.message}")
+ // 토큰 재발급 실패 시 토큰 클리어 (민감정보 비노출)
+ Log.d("TokenAuthenticator", "토큰 재발급 실패: ${e.message}")
tokenManager.clearToken()
nullAlso applies to: 57-62, 50-51 |
||
| } catch (e: Exception) { | ||
| // 토큰 재발급 실패 시 토큰 클리어 | ||
| Log.d("TokenAuthenticator", "토큰 재발급 실패: ${e.message}") | ||
| tokenManager.clearToken() | ||
| null | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private suspend fun getNewToken(refreshToken: String): ReissueTokenResponse? { | ||
| val loggingInterceptor = HttpLoggingInterceptor() | ||
| loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY | ||
| val okHttpClient = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() | ||
|
|
||
| val retrofit = Retrofit.Builder() | ||
| .baseUrl(BuildConfig.BASE_URL) | ||
| .addConverterFactory( | ||
| Json.asConverterFactory(requireNotNull("application/json".toMediaType())) | ||
| ) | ||
| .client(okHttpClient) | ||
| .build() | ||
| val service = retrofit.create(AuthService::class.java) | ||
| return service.reissueToken(refreshToken).response | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ import androidx.datastore.preferences.core.stringPreferencesKey | |
| import androidx.datastore.preferences.preferencesDataStore | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||
| import kotlinx.coroutines.flow.asSharedFlow | ||
| import kotlinx.coroutines.flow.map | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
@@ -14,6 +16,9 @@ private val Context.dataStore by preferencesDataStore(name = "user_prefs") | |
|
|
||
| @Singleton | ||
| class TokenManager @Inject constructor(@ApplicationContext private val context: Context) { | ||
| private val _logoutEvent = MutableSharedFlow<Unit>() | ||
| val logoutEvent = _logoutEvent.asSharedFlow() | ||
|
|
||
|
Comment on lines
+19
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SharedFlow 기본값으로 emit이 영구 대기할 수 있음 (OkHttp 스레드 정지 위험) + 타입 명시 제안 clearToken()에서 적용 diff: @@
-import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@@
- private val _logoutEvent = MutableSharedFlow<Unit>()
- val logoutEvent = _logoutEvent.asSharedFlow()
+ private val _logoutEvent = MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1)
+ val logoutEvent: SharedFlow<Unit> = _logoutEvent.asSharedFlow()
@@
- _logoutEvent.emit(Unit)
+ _logoutEvent.tryEmit(Unit)Also applies to: 51-57, 9-11 🤖 Prompt for AI Agents |
||
| companion object { | ||
| private val ACCESS_TOKEN = stringPreferencesKey("access_token") | ||
| private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") | ||
|
|
@@ -48,5 +53,6 @@ class TokenManager @Inject constructor(@ApplicationContext private val context: | |
| preferences.remove(ACCESS_TOKEN) | ||
| preferences.remove(REFRESH_TOKEN) | ||
| } | ||
| _logoutEvent.emit(Unit) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retrofit+kotlinx-serialization에서 @Body String은 변환 불가 → 요청 모델 필요
Json 컨버터만 등록된 상태에서
@Body String은 지원되지 않습니다. 런타임에 “no converter for String” 오류가 납니다. 요청 DTO를 정의해 주세요.적용 diff:
추가 파일(예시):
Also applies to: 3-5
🤖 Prompt for AI Agents