Skip to content

Commit

Permalink
Authenticator Retry 로직 수정 및 토큰 관리, 에러 처리 보완 (#189)
Browse files Browse the repository at this point in the history
* Authenticator 함수 내부 구현 변경

401 로 떨어질 경우 refresh 토큰을 헤더에 담아 호출하는 가장 간단한 형태로 변경

* RefreshTokenExpiredException 추가

* Refresh 토큰도 만료될 경우, 로그인 토큰을 제거하고, 로그인 화면으로 돌아가는 로직 구현

내 정보 화면만 해당, 다른 화면도 동일 로직을 적용 시켜야 함

* 회원 가입 API @get -> @post 로 수정, 로그인 뷰모델 에러 핸들링 로직 변경

* 회원 탈퇴할 경우 로그인 토큰을 제거하는 로직 추가

* chore: 필요하지 않은 코드 제거 및 suppress 구문 제거

* chore: @Suppress annotation 원복 및 주석 처리된 코드 제거
  • Loading branch information
easyhooon authored Jan 2, 2024
1 parent 2ad0b41 commit e3643f2
Show file tree
Hide file tree
Showing 10 changed files with 59 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import javax.inject.Inject
import us.wedemy.eggeum.android.data.datastore.TokenDataStoreProvider

public class LoginLocalDataSourceImpl @Inject constructor(
// private val dataStore: TokenDataStore
private val dataStore: TokenDataStoreProvider,
) : LoginLocalDataSource {
override suspend fun setAccessToken(accessToken: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

package us.wedemy.eggeum.android.data.extensions

import java.io.IOException
import java.net.UnknownHostException
import retrofit2.HttpException

internal fun Exception.toAlertMessage(): String {
return when (this) {
is HttpException -> "서버에 문제가 발생했어요. 잠시 후 다시 시도해주세요."
is UnknownHostException -> "네트워크 연결을 확인해주세요."
is IOException -> "로그인 토큰이 만료되었어요. 다시 로그인 해주세요"
else -> "예기치 못한 오류가 발생했어요. 잠시 후 다시 시도해주세요."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package us.wedemy.eggeum.android.data.service

import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import us.wedemy.eggeum.android.data.model.login.LoginRequest
import us.wedemy.eggeum.android.data.model.login.LoginResponse
Expand All @@ -23,7 +22,7 @@ public interface LoginService {
@Body loginRequest: LoginRequest,
): Response<LoginResponse>

@GET("app/sns-sign-up")
@POST("app/sns-sign-up")
public suspend fun signUp(
@Body signUpRequest: SignUpRequest,
): Response<SignUpResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@
package us.wedemy.eggeum.android.data.service

import javax.inject.Inject
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject
import timber.log.Timber
import us.wedemy.eggeum.android.data.datastore.TokenDataStoreProvider
import us.wedemy.eggeum.android.domain.util.RefreshTokenExpiredException

@Suppress("TooGenericExceptionCaught")
public class TokenAuthenticator @Inject constructor(
private val dataStoreProvider: TokenDataStoreProvider,
) : Authenticator {

override fun authenticate(route: Route?, response: Response): Request? {
val accessToken = runBlocking {
dataStoreProvider.getAccessToken()
}

if (hasNotAccessTokenOnResponse(response)) {
synchronized(this) {
val newAccessToken = runBlocking {
dataStoreProvider.getAccessToken()
}
if (accessToken != newAccessToken) {
return newRequestWithAccessToken(response.request, newAccessToken)
}
Timber.d("authenticate 호출")
return runBlocking {
val currentAccessToken = dataStoreProvider.getAccessToken()

// 현재 액세스 토큰이 요청 헤더의 토큰과 다르면 이미 갱신된 것으로 간주
if (response.request.header("Authorization") != "Bearer $currentAccessToken") {
Timber.d("RefreshToken is Expired")
// 로그인 토큰 제거(로그아웃)
dataStoreProvider.clear()
throw RefreshTokenExpiredException
}

val refreshToken = runBlocking {
dataStoreProvider.getRefreshToken()
}
return newRequestWithAccessToken(response.request, refreshToken)
try {
val newAccessToken = runBlocking { dataStoreProvider.getRefreshToken() }
newRequestWithAccessToken(response.request, newAccessToken)
} catch (e: Exception) {
Timber.e("TokenAuthenticator Error :: ${e.message}")
null
}
}

return null
}

private fun hasNotAccessTokenOnResponse(response: Response): Boolean =
response.header("Authorization") == null

private fun newRequestWithAccessToken(request: Request, accessToken: String): Request =
request.newBuilder()
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $accessToken")
.header("Authorization", "Bearer $accessToken")
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

package us.wedemy.eggeum.android.data.util

import java.io.IOException
import java.net.UnknownHostException
import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber
import us.wedemy.eggeum.android.data.extensions.toAlertMessage

@Suppress("TooGenericExceptionCaught")
Expand All @@ -21,28 +21,29 @@ internal suspend fun <T> safeRequest(request: suspend () -> Response<T>): T? {
return response.body()
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Timber.d(Exception(errorBody))
throw ExceptionWrapper(
statusCode = response.code(),
message = Exception(errorBody).toAlertMessage(),
cause = Exception(errorBody),
)
}
} catch (exception: HttpException) {
Timber.d(exception)
throw ExceptionWrapper(
statusCode = exception.code(),
message = exception.response()?.errorBody()?.string() ?: exception.message(),
cause = exception,
)
} catch (exception: UnknownHostException) {
Timber.d(exception)
throw ExceptionWrapper(
message = exception.toAlertMessage(),
cause = exception,
)
} catch (exception: IOException) {
throw ExceptionWrapper(
message = exception.toAlertMessage(),
cause = exception,
)
} catch (exception: Exception) {
Timber.d(exception)
throw ExceptionWrapper(
message = exception.toAlertMessage(),
cause = exception,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import java.io.IOException
public val LoginApiResponseIsNull: IOException = IOException("Login API response is null.")
public val LoginApiResponseNotFound: IOException = IOException("Login API returned Not Found.")
public val SignUpApiResponseIsNull: IOException = IOException("SignUp API response is null.")
public val RefreshTokenExpiredException: IOException = IOException("Refresh token is expired.")

// Enum
public val EnumApiResponseIsNull: IOException = IOException("The Enum API response is null.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ class LoginViewModel @Inject constructor(
result.isFailure -> {
val exception = result.exceptionOrNull()
Timber.d(exception)
if (exception == LoginApiResponseNotFound) {
_navigateToOnBoardingEvent.emit(Unit)
} else {
_showToastEvent.emit(exception?.message ?: "Unknown Error Occured")
if (exception != null) {
if (exception.cause == LoginApiResponseNotFound) {
_navigateToOnBoardingEvent.emit(Unit)
} else {
_showToastEvent.emit(exception.message ?: "Unknown Error Occured")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import us.wedemy.eggeum.android.common.ui.BaseFragment
import us.wedemy.eggeum.android.design.R
import us.wedemy.eggeum.android.main.databinding.FragmentMyAccountBinding
import us.wedemy.eggeum.android.main.model.UserInfoModel
import us.wedemy.eggeum.android.main.ui.MainActivity
import us.wedemy.eggeum.android.main.viewmodel.MyAccountViewModel

@AndroidEntryPoint
Expand Down Expand Up @@ -82,6 +83,12 @@ class MyAccountFragment : BaseFragment<FragmentMyAccountBinding>() {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
}

launch {
viewModel.navigateToLoginEvent.collect {
(activity as MainActivity).navigateToLogin()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import us.wedemy.eggeum.android.domain.usecase.GetUserInfoUseCase
import us.wedemy.eggeum.android.domain.util.RefreshTokenExpiredException
import us.wedemy.eggeum.android.main.mapper.toUiModel
import us.wedemy.eggeum.android.main.model.ProfileImageModel

Expand All @@ -43,6 +44,9 @@ class MyAccountViewModel @Inject constructor(
private val _showToastEvent = MutableSharedFlow<String>()
val showToastEvent: SharedFlow<String> = _showToastEvent.asSharedFlow()

private val _navigateToLoginEvent = MutableSharedFlow<Unit>()
val navigateToLoginEvent: SharedFlow<Unit> = _navigateToLoginEvent.asSharedFlow()

fun getUserInfo() {
viewModelScope.launch {
val result = getUserInfoUseCase()
Expand All @@ -62,8 +66,13 @@ class MyAccountViewModel @Inject constructor(
}
result.isFailure -> {
val exception = result.exceptionOrNull()
Timber.d(exception)
_showToastEvent.emit(exception?.message ?: "Unknown Error Occured")
Timber.e(exception)
if (exception != null) {
if (exception.cause == RefreshTokenExpiredException) {
_navigateToLoginEvent.emit(Unit)
}
_showToastEvent.emit(exception.message ?: "Unknown Error Occured")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import us.wedemy.eggeum.android.common.util.getMutableStateFlow
import us.wedemy.eggeum.android.domain.usecase.LogoutUseCase
import us.wedemy.eggeum.android.domain.usecase.WithdrawUseCase

@HiltViewModel
class WithdrawViewModel @Inject constructor(
private val withdrawUseCase: WithdrawUseCase,
private val logoutUseCase: LogoutUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _agreeToWithdraw = savedStateHandle.getMutableStateFlow(KEY_AGREE_TO_NOTIFICATION, false)
Expand All @@ -43,6 +45,7 @@ class WithdrawViewModel @Inject constructor(
val result = withdrawUseCase()
when {
result.isSuccess -> {
logoutUseCase()
_navigateToLoginEvent.emit(Unit)
}
result.isFailure -> {
Expand Down

0 comments on commit e3643f2

Please sign in to comment.