[Android] - Authenticator를 활용해 JwtToken 갱신하기
개요
웹개발이나, 클라이언트에서 사용자 인증을 위해서 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에서 처리를 하는 경우도 있는 것을 확인했습니다.
참고