Skip to content

Instantly share code, notes, and snippets.

@SANDY-9
Last active April 10, 2024 14:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SANDY-9/87603f7caae6a7583bcd15005e83fb7f to your computer and use it in GitHub Desktop.
Save SANDY-9/87603f7caae6a7583bcd15005e83fb7f to your computer and use it in GitHub Desktop.
AccessTokenAuthenticator
import android.content.SharedPreferences
import com.example.network.refresh.model.Token
import okhttp3.Authenticator
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.Route
import java.io.IOException
class TokenInvalidException : IOException("TOKEN_INVALID")
class AccessTokenAuthenticator(
private val prefs: SharedPreferences
) : Authenticator {
/**
* authenticate() : 401 에러가 반환된 요청에 대해 실행되며 다시 실행할 수 있는 재요청을 반환하는 콜백
* 이 때, parallel한 비동기 요청들에 대해 refresh api 호출은 단 한번만 이뤄져야 하므로 authenticate는 @Synchronized하게 실행되어야함
* @response: 401을 반환하는 요청들 -> 이 요청들을 다시 재실행하는 Request를 리턴함
*/
@Synchronized
override fun authenticate(route: Route?, response: Response): Request? {
val message = response.body?.string() ?: return null
return when {
message.contains("TOKEN_INVALID") -> throw TokenInvalidException()
message.contains("TOKEN_EXPIRED") -> handleTokenExpiration(response.request)
else -> null
}
}
private fun handleTokenExpiration(oldRequest: Request): Request? {
// 현재 로컬에 저장되어 있는 AccessToken값
val currentAccessToken = TokenStoreUtils.getAccessTokens(prefs)
val authorizationHeader = oldRequest.header("Authorization")
/**
* 가장 먼저 authenticate를 실행하는 요청에 대해서만 refresh api를 호출할 수 있도록 판단
* 1. 401을 반환했던 요청들의 헤더를 로컬에 저장되어 있는 AccessToken값과 비교해 refresh()를 호출해야하는지 여부 판단
* 2. 401을 반환했던 요청들은 전부 헤더에 만료된 AccessToken값을 가지고 있음
* 3. 첫번째로 도달된 요청이 만료된 토큰을 갱신한다면 refresh 요청이 수행 된다면
* -> 401을 반환했던 나머지 요청들의 헤더에 포함 되어 있는 AccessToken값과 로컬에 새로 저장된 토큰 값이 다를 것임
*/
val shouldRefreshToken = authorizationHeader?.contains("$currentAccessToken") ?: true
// refresh가 필요 없을 때 -> refresh api를 호출해 새롭게 갱신하는 과정 불필요
// refresh가 필요할 때 -> shouldRefreshToken == true -> refresh api를 호출하는 과정을 수행한 후 리턴된 새로운 토큰
val newRequestAccessToken =
if (shouldRefreshToken) getRefreshedAccessToken() else currentAccessToken
// oldRequest를 새로운 헤더로 교체해서 재요청하기 위한 newRequest 생성
return oldRequest.newBuilder()
.header("Authorization", "Bearer $newRequestAccessToken")
.build()
}
// refresh api를 호출하고 갱신된 refreshToken과 accessToken을 새롭게 저장하고 새로운 AccessToken을 리턴한다
private fun getRefreshedAccessToken(): String? {
return try {
val refreshToken = TokenStoreUtils.getRefreshTokens(prefs)
val body = RequestBody.create("text/plain".toMediaTypeOrNull(), refreshToken!!)
val response = PracticeApi.invoke(prefs).refreshToken(body).execute()
if (response.isSuccessful) {
val token = response.body()!!
updateTokensToLocal(token)
token.accessToken //새로운 AccessToken을 리턴
} else {
// logic to handle failure (logout, etc)
null
}
} catch (e: Exception) {
null
}
}
//갱신된 refreshToken과 accessToken을 새롭게 로컬에 저장
private fun updateTokensToLocal(token: Token) {
TokenStoreUtils.updateAccessTokens(prefs, token.accessToken)
TokenStoreUtils.updateRefreshToken(prefs, token.refreshToken)
}
}
@SANDY-9
Copy link
Author

SANDY-9 commented Apr 3, 2024

위의 코드에서 fun refreshAccessToken()의 val response = runBlocking(Dispatchers.IO) {
PracticeApi.invoke(prefs).refreshToken(body)
}
구문은 실행되지 않아 runBlocking상태를 유지하기 때문에 ANR이 발생한다.
*runBlocking{} 블록은 주어진 블록이 완료될때까지 현재 스레드를 멈추는 새로운 코루틴을 생성하여 실행하는 코루틴 빌더이기 때문
refreshToken() 함수가 suspand fun 중단함수이기 때문에 중단 함수는 runBlocking {} 안에서는 실행되지 않는다.

@POST("auth/refresh")
fun refreshToken(
    @Body refreshToken: RequestBody
): Call<Token>

val response = PracticeApi.invoke(prefs).refreshToken(body).execute()

refreshToken()를 중단함수로 사용하지 않고 Call을 리턴하도록 변경

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment