Skils/Android

[Android] - Authenticator를 활용해 JwtToken 갱신하기

재한 2024. 10. 12. 17:54

개요

웹개발이나, 클라이언트에서 사용자 인증을 위해서 JwtToken을 도입하고 있습니다. 

안드로이드에서도 JwtToken을 기존에 처리했었는데, 이번 프로젝트에서 Authenticator를 활용해서 처리해 봤습니다.

그에 대한 내용을 포스팅할까 합니다.

 

 

Authenticator

 

허락되지 않은 사용자, 인증되지 않은 사용자일 경우 HTTP 상태코드 중 401을 받게 됩니다.

 

이전에는 401 에러를 받을 경우 갱신 요청을 보내는 로직을 작성했다면, 네트워크 통신 단계에서 401에러를 검증하고 작성한 로직을 실행시킬 수 있는 Authenticator에 존재를 알게 되었습니다.

(사실 이 전에 몰랐던 것이 말이 안 된다..라고 생각이 들긴 합니다)

class JwtAuthenticator
    @Inject
    constructor(
        private val tokenDataSource: TokenDataSource,
        private val authApi: AuthApi,
    ) : Authenticator {
        override fun authenticate(
            route: Route?,
            response: Response,
        ): Request? {
            val request = response.request
            if (request.header("Authorization").isNullOrEmpty()) {
                return null
            }
            val refreshToken =
                runBlocking {
                    tokenDataSource.getRefreshToken()
                }
            return try {
                val newJwtToken: JwtToken? =
                    runBlocking {
                        val result =
                            authApi.getNewToken(
                                refreshToken ?: "",
                            )
                        result.data
                    }
                if (newJwtToken == null) return null
                runBlocking { tokenDataSource.saveJwtToken(newJwtToken) }

                request
                    .newBuilder()
                    .removeHeader("Authorization")
                    .addHeader("Authorization", newJwtToken.accessToken)
                    .build()
            } catch (e: Throwable) {
                when (e) {
                    is ApiException -> throw e
                    is RefreshTokenExpiredException -> throw e
                    is IOException -> throw IOException(e)

                    else -> throw e
                }
            }
        }
    }


Authenticator를 401 에러가 발생할 경우 실행되기 때문에, 그에 맞는 동작을 구현하려고 노력했습니다.

TokenDataSource에 있는 JwtToken을 활용해서 서버로 토큰 갱신을 처리하는 로직인데요,

처음으로 request header에 AccessToken이 없다면 넘어갑니다.

Header에 AccessToken 정보가 있을 경우에는 저장된 refreshToken을 가져옵니다.

해당 과정에서 RunBlocking이 사용된 이유는 동기적으로 진행되어야 하기 때문에 토큰을 가져오는 로직을 진행한 후
아래 코드가 실행되도록 의도했습니다.

위에서 가져온 JwtToken의 RefreshToken을 바탕으로 서버에 갱신 요청을 하고, 해당 토큰을 저장한 후 기존의 헤더를 지우고, 새로운 헤더를 만들어 AccessToken을 담아냅니다.

 

구현한 Authenticator를 Retrofit에 추가해 줍니다.

 

 

의존성 사이클 형성

 

하지만 해당 코드를 빌드하면 오류가 발생하게 됩니다.

 

오류 내용은 정말 길지만, 간단하게 요약하면 의존성 주입에 대한 사이클이 발생했다는 것을 알 수 있었습니다.

 

현재 구현한 JwtAuthenticator는 AuthApi를 주입받고 있는데, AuthApi를 구현하기 위해 사용될 Retrofit에서 다시 OkHttpClient를 주입받고 있기 때문에 의존성 사이클이 발생한 것이 원인이었습니다.

 

 

JwtAuthenticator -> AuthApi -> OkHttpClient -> JwtAuthenticator라는 사이클을 형성하게 됩니다.

 

사실 서버와의 통신에서 AccessToken을 필요로 하지 않는 API가 존재하기 때문에 해당 부분을 덜어낼 새로운 OkHttpClient 객체를 만들어 분리를 해야 했습니다.

 

즉 AuthAPI를 사용할 OkHttpClient객체와 사용하지 않을 OkHttpClient를 분리해서 해당 OkHttpClient에는 Authenticator를 주입하지 않게 하는 것이 해결방법이었습니다.

 

해당 설계를 통해 JwtToken을 갱신하는 Authenticator와 AuthApi를 분리할 수 있었고, 의존성 사이클을 해소할 수 있었습니다.

 

그러면 Retrofit이 OkHttpClient 객체를 구분할 수 있어야 합니다.

저는 @Qualifer를 이용해  AuthApi를 사용하는 객체와 아닌 객체를 분리했습니다.

이후 OkHttpClient는 다음과 같이 구현할 수 있습니다.

위에 설계도대로, BaseClient는 JwtAuthenticator를 주입하고, AuthClient는 주입하지 않았습니다.

이후 ApiModule에는

AuthApi에는 AuthClient를, 다른 서버 통신에는 BaseClient를 주입하면 해결됩니다.

 

 


 

이번에 Authenticator라는 존재를 알게 되었고 활용해 봤습니다. 하지만 조사해 본 결과 401 에러 외에도 407 에러를 받을 경우 

Authenticator가 실행된다고 하더라고요. 그리고 이번처럼 서버와 인증되지 않은 사용자일 경우 401 에러를 주지 않을 경우는 해당 코드의 도입을 고려해봐야 할 것 같다는 생각이 들었습니다.. 그래서 몇몇의 코드는 Authenticator가 아닌 Interceptor에서 처리를 하는 경우도 있는 것을 확인했습니다.

 

 

참고

 

안드로이드 만료 토큰 갱신 / 요청 api에 토큰 삽입 자동화 시스템 개발기 - 3 Interceptor에서 Token 삽

이번 편은 Retrofit Interface에서 Token Parameter를 없앤 방법에 대해서 이야기할 것이다. 먼저, 사내 인증 시스템에서는 토큰을 이중으로 발급 받아야하는 상황이라고 적었었다. 라마인드 하자면, 아래

modelmaker.tistory.com

 

 

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

레트로핏은 안드로이드에서 서버와 REST API 통신을 위해 주로 사용되는 라이브러리이다. OkHttp를 기반으로 동작하며 높은 성능과 뛰어난 가독성, 쉬운 유지보수 등의 이유로 가장 많이 사용되는

velog.io