Retrofit2를 사용해 JWT 토큰 인증하기 -2

TRASALBY·2023년 4월 19일
1

Android

목록 보기
6/10
post-custom-banner

만료된 토큰 갱신 요청하기

이전 글에서 작성 하였듯 현재 가지고 있는 accessToken으로 서비스 요청을 수행하지만 발급 된지 오래되었다면 이미 만료된 accessToken일 수 있다. 이 경우는 refreshToken을 사용해 사용자의 accessToken을 재발급 받아야한다.

Authenticator로 인증

Authenticator은 서버에서 인증이 필요한 경우 401상태 코드를 포함된 응답을 반환받았을 때 실패한 요청과 401상태코드를 함께 authenticate()를 호출한다.

이경우 authenticate 메서드 에서는 필요한 인증 정보를 포함하는 새로운 요청을 반환하여야 하고 인증 처리를 할 수 없다면 null을 반환하여야 한다.

 	@Singleton
    @Provides
    fun provideTokenAuthenticator(
        @ApplicationContext context: Context,
        authApi: AuthApi
    ) = Authenticator { _, response ->
        val tag = "TokenAuthenticator"
        val isPathRefresh =
            response.request.url.toUrl().toString() == BuildConfig.BASE_URL + "auth/refresh"
        if (response.code == 401 && !isPathRefresh) {
            try {
                val refreshToken = EncryptedPrefs.getString(PrefsKey.REFRESH_TOKEN_KEY)
                val tokenRefreshRequest = TokenRefreshRequest(refreshToken)
                val resp = authApi.tokenRefresh(tokenRefreshRequest).execute()
                EncryptedPrefs.clearPrefs()

                if (!resp.isSuccessful) {
                    IntroActivity.startActivity(context)
                    throw TokenRefreshFailedException("토큰 갱신 실패")
                }

                val token = resp.body() ?: throw TokenEmptyException("받아온 토큰 값이 null임")

                EncryptedPrefs.putString(PrefsKey.ACCESS_TOKEN_KEY, token.accessToken)
                EncryptedPrefs.putString(PrefsKey.REFRESH_TOKEN_KEY, token.refreshToken)

                response.request.newBuilder().apply {
                    removeHeader("Authorization")
                    addHeader("Authorization", "Bearer ${token.accessToken}")
                }.build()
            } catch (e: Exception) {
                Log.e(tag, e.message.toString(), e)
            }
        }
        null
    }

이것 역시 hilt로 주입해주었다. 구현된 코드의 동작 흐름을 따라가보자

  1. 서버에 accessToken을 담은 요청이 인증을 실패하여 Authenticator에서 감지하였다.
  2. 저장되어 있던 refreshToken을 사용해 토큰을 갱신하는 요청을 수행한다.
  3. 정상적으로 토큰이 갱신되었다면 새로운 토큰을 로컬에 저장하고 새로 발급받은 token을 헤더로 넣어주어 다시 이전의 요청을 진행한다.
  4. 토큰을 갱신하는 요청에서도 에러가 발생하였다는 것은 refreshToken마저 만료되었다는 뜻이다. 이경우는 IntroActivity로 사용자를 이동시켜 다시 로그인 과정을 통해 새로운 인증을 수행하도록 한다.

별도의 OkHttpClient 생성

위의 과정으로 토큰 갱신과정이 문제없이 수행될 것이라 기대하였지만 새로운 문제가 발생하였다.

    fun provideBabaClient(
        authorizationInterceptor: Interceptor,
        tokenAuthenticator: Authenticator
    ): OkHttpClient {
        val builder = OkHttpClient.Builder()
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        builder.apply {
            addInterceptor(loggingInterceptor)
            addInterceptor(authorizationInterceptor)
        }
        builder.authenticator(tokenAuthenticator)
        return builder.build()
    }

위와 같이 OkhttpClient를 생성하고 모든 Retrofit객체에서 사용하게 될 경우 문제가 발생한다.
TokenAuthenticator에서 AuthApi를 주입받고 있는데 해당 API를 구현할 때 주입받는 Retrofit 객체에서 다시 OkhttpClient를 주입받고 있다.
정리하자면
TokenAuthenticator -> AuthApi -> Retrofit -> OkHttpClient -> TokenAuthenticator 의 순서로 구현을 기다리게 되는 무한 루프에 빠지게 된다.

이를 해결하기 위해 AuthApi에서 사용할 Retrofit에서는 Authenticator을 가지지 않는 별도의 OkHttpClient를 구현해 줄 필요가 있었다.


    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class BabaClient

    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class AuthClient



    @BabaClient
    @Singleton
    @Provides
    fun provideBabaClient(
        authorizationInterceptor: Interceptor,
        tokenAuthenticator: Authenticator
    ): OkHttpClient {
        val builder = OkHttpClient.Builder()
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        builder.apply {
            addInterceptor(loggingInterceptor)
            addInterceptor(authorizationInterceptor)
        }
        builder.authenticator(tokenAuthenticator)
        return builder.build()
    }

    @AuthClient
    @Singleton
    @Provides
    fun provideAuthClient(
        authorizationInterceptor: Interceptor
    ): OkHttpClient {
        val builder = OkHttpClient.Builder()
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        builder.apply {
            addInterceptor(loggingInterceptor)
            addInterceptor(authorizationInterceptor)
        }
        return builder.build()
    }

힐트에서 같은 타입의 객체를 구분하여 주입받기 원할때는 Qualifier를 통해 새로운 어노테이션을 생성하고 그것을 붙여주어 구분할 수 있다.

	fun provideAuthRetrofit(
        @NetworkModule.AuthClient
        okHttpClient: OkHttpClient,
        gsonConverterFactory: GsonConverterFactory
    ): Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(gsonConverterFactory)
        .build()

필요한 Client타입을 어노테이션으로 구분하여 적절하게 주입해주어 문제를 해결하였다.

post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 4월 16일

잘 보고 갑니다!

답글 달기