
대부분 프로젝트를 진행하면 서버에서 사용자 인증을 JWT 사용하여 처리하는 경우가 많을 것 같습니다. 서버에서 전달해준 Token을 받게 되면 로컬 저장소에 저장한 후 API 요청 시에 HTTP Header에 추가하여 전달해야 합니다. Retrofit 라이브러리를 사용할 경우 API 호출 시 매개변수를 통해 전달해도 되지만, API 가 많아질수록 계속 매개변수로 전달하기는 좋은 방식이 아닙니다. 이를 Interceptor를 사용하면 손쉽게 구현할 수 있습니다.
JWT(Json Web Token)은 Json 포맷을 이용하여 사용자 신원, 권환에 대한 정보들을 저장하는 Token입니다.

하지만 Access Token 전달 과정에서 탈취를 당하면 누구나 그 사용자에 대한 권한으로 인증을 통과할 수 있습니다. 이를 위해서 AccessToken과 RefreshToken 2개를 사용하여 보통 권한 인증을 하게 됩니다.
- AccessToken : 만료기간이 짧음
- RefreshToken : 만료기간이 길다
- AccessToken이 만료되었을 때 RefreshToken을 사용하여 서버에게 새로운 AccessToken을 발급받게 되고 발급 받은 AccessToken을 내부저장소에 저장을 하여 서버 요청에 사용하게 됩니다.
- RefreshToken이 탈취되면 더 큰 문제를 야기할 수 있지만, AccessToken에 비해 서버에 전달할 일은 많이 없기 때문에 더 안전한 방식이라고 할 수 있습니다.
위 같이 서버에서 준 AccessToken과 RefreshToken을 받아 API 사용 시에 AcessToken을 전달하고 AccessToken이 만료되었을 때 RefreshToken으로 갱신을 하는 방법을 OkHttp의 Interceptor를 사용하면 손쉽게 구현할 수 있습니다.

Interceptor는 API 요청을 모니터링하거나 재요청할 수 있는 강력한 메커니즘입니다. 아래 그림처럼 Application이나 Network Interceptor를 사용하면 기존의 요청이 아닌 추가적인 작업을 한 후 새로운 요청으로 보낼 수 있습니다.
class BuddyConInterceptor @Inject constructor(
private val tokenRepository: TokenRepository,
private val userRepository: UserRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val tokenInfo = runBlocking {
combine(
tokenRepository.getToken(),
tokenRepository.getRefreshToken(),
tokenRepository.getTokenExpiration()
) { accessToken, refreshToken, accessTokenExpiresIn ->
UserInfo(accessToken, refreshToken, accessTokenExpiresIn)
}.first()
}
var (accessToken, refreshToken, accessTokenExpiresIn) = tokenInfo
val currentTime = System.currentTimeMillis()
if (accessTokenExpiresIn < currentTime) {
try {
val refreshTokenInfo = runBlocking {
userRepository.requestRefreshToken(accessToken, refreshToken).first()
}
accessToken = refreshTokenInfo.accessToken
refreshToken = refreshTokenInfo.refreshToken
accessTokenExpiresIn = refreshTokenInfo.accessTokenExpiresIn
runBlocking { tokenRepository.saveToken(accessToken, accessTokenExpiresIn, refreshToken) }
} catch (_: Exception) {
}
}
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $accessToken")
.build()
return chain.proceed(newRequest)
}
}
BuddyCon에서는 TokenRespotiroy에서 DataStore를 활용하여 AccessToken과 AccessToken의 만료일, RefreshToken을 저장하였습니다. 그리고 API 요청 시마다 Interceptor를 사용하여 AceesToken의 유효성을 판단하고 유효하지 않을 경우 RefreshToken으로 AccessToken을 갱신시킵니다.
RefreshToken 만료가 되어 AccessToken을 갱신하지 못할 경우에는 API 호출하는 부분에서 권한 없음(403) 에러가 나타날 것이고 예외처리만 하면 됩니다 😊
@BuddyConClient
@Provides
@Singleton
fun provideBuddyConClient(
@HttpLoggingInterceptorQualifier httpLoggingInterceptor: Interceptor,
@BuddyConInterceptorQualifier buddyConInterceptor: Interceptor
): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(buddyConInterceptor)
.build()
BuddyConInterceptor를 만들었으면 OKHttpClient 생성 시에 addInterceptor 또는 addNetworkInterceptor 함수를 통해 전달하면 됩니다. 위 코드는 Hilt 를 통해 의존성 주입을 하였고 하나의 Interceptor가 아닌 두개의 Interceptor를 구별하기 위해 Qualifier를 사용하였습니다. 이부분에서는 다음에 포스팅할 예정입니다 :)