[Android] Retrofit Interceptor로 헤더에 액세스 토큰 넣기

문승연·2024년 2월 11일
0

Android 기초

목록 보기
6/8

1. Retrofit Interceptor

안드로이드 앱에서 백엔드 서버와 네트워크 통신을 할 때 가장 많이 사용하는 라이브러리는 아마 Retrofit일 것이다.

서버에 API 요청을 할 때 사용자 인증을 위해서 OAuth2.0 방식으로 발급받은 액세스 토큰을 헤더에 넣어서 요청해야하는 경우가 많은데 매 요청마다 일일이 액세스토큰을 넣어야 한다면 여간 귀찮은 게 아니다.

이런 상황에서 유용하게 사용할 수 있는 게 바로 Interceptor다.

Interceptor를 사용하면 매 API 요청을 모니터링하면서 필요에 따라 인증, 재요청 등의 작업을 수행할 수도 있다.

Interceptor는 크게 1. Application Interceptor2. Network Interceptor 두 종류가 있다.

일반적으로 addInterceptor 메소드를 이용해서 추가하는 Interceptor들은 전부 Application Interceptor이다. 우리가 Request 헤더에 넣어줘야하는 액세스 토큰은 디바이스에 저장되어있기 때문에 이 역시 Application Interceptor에서 해줘야하는 역할이다.

반면 Network Interceptor는 앱의 콘텐츠와는 직접적 관계가 없는, Network의 상태 및 서버 값에 따라서 retry 하는 등의 로직을 넣는 곳이다.

NetworkModule.kt

...

@Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient =
	OkHttpClient.Builder()
		.connectTimeout(ConnectTimeout, TimeUnit.SECONDS)
		.writeTimeout(WriteTimeout, TimeUnit.SECONDS)
		.readTimeout(ReadTimeout, TimeUnit.SECONDS)
		.addInterceptor(
			HttpLoggingInterceptor().apply {
				level = HttpLoggingInterceptor.Level.BODY
			}
		)
		.build()

...

기존의 Retrofit을 구성하는 OkHttpClient 구현하는 코드는 위와 같다. 여기서 이제 TokenInterceptor 클래스를 생성해 addInterceptor() 메소드로 추가해줄 것이다.

2. TokenInterceptor 구현

TokenInterceptor.kt

class TokenInterceptor @Inject constructor(
	private val localTokenDataSource: LocalTokenDataSource
) : Interceptor {

	override fun intercept(chain: Interceptor.Chain): Response {
		return runBlocking {
			val accessToken = localTokenDataSource.getAccessToken().first()
			val request = if (accessToken.isNotEmpty()) {
				chain.request().putTokenHeader(accessToken)
			} else {
				chain.request()
			}
			chain.proceed(request)
		}
	}

	private fun Request.putTokenHeader(accessToken: String): Request {
		return this.newBuilder()
			.addHeader(AUTHORIZATION, "Bearer $accessToken")
			.build()
	}

	companion object {
		private const val AUTHORIZATION = "authorization"
	}
}

getAccessToken() 메소드가 DataStore.Preferences에서 Flow로 데이터를 가져오는 비동기 작업이기 때문에 runBlocking 블럭으로 전체를 감싸준다.

3. Authenticator 구현

여기서 끝이 아니다. 만약 액세스 토큰의 유효 기간이 만료되었거나 권한이 없는 다른 토큰을 헤더에 넣었다면 권한이 없다는 응답값을 서버로부터 받을 것이다.

이 경우 토큰 재발급을 서버에 요청해야하는데 이 과정을 유저가 알아차리지 못하게 앱에서 알아서 처리하게 만들고 싶다.

즉, 다음과 같은 프로세스로 통신이 진행된다.

  1. TokenInterceptor 로 액세스 토큰을 헤더에 넣어서 요청 전송.
  2. 요청에 포함된 액세스 토큰이 만료되거나 권한이 없을 경우 401 에러 응답.
  3. Authenticator에서 401 응답 감지시 디바이스에 저장된 리프레시 토큰으로 새 토큰 발급 요청
  4. 새 토큰 발급 받으면 디바이스에 저장 후 갱신된 토큰으로 1번의 요청 다시 전송

AuthAuthenticator.kt

class AuthAuthenticator @Inject constructor(
	private val localTokenDataSource: LocalTokenDataSource
) : Authenticator {

	private companion object {
		private const val ConnectTimeout = 15L
		private const val WriteTimeout = 20L
		private const val ReadTimeout = 15L
		private val contentType = "application/json".toMediaType()
	}

	override fun authenticate(route: Route?, response: Response): Request {
		if (response.code == HTTP_UNAUTHORIZED) {
			val token = runBlocking {
				localTokenDataSource.getRefreshToken().first()
			}
			// The access token is expired. Refresh the credentials.
			synchronized(this) {
				// Make sure only one coroutine refreshes the token at a time.
				return runBlocking {
					val newTokenResult = getNewToken(token)
					if (newTokenResult is Result.Success) {
						val accessToken = newTokenResult.body!!.result.accessToken
						val refreshToken = newTokenResult.body.result.refreshToken
						// Update the access token in your storage.
						localTokenDataSource.updateAccessToken(accessToken)
						localTokenDataSource.updateRefreshToken(refreshToken)
						return@runBlocking response.request.newBuilder()
							.header("Authorization", "Bearer $accessToken")
							.build()
					} else {
						return@runBlocking response.request
					}
				}
			}
		}
		return response.request
	}
}

4. Interceptor, Authenticator 적용

fun providesLyfeOkHttpClient(tokenInterceptor: TokenInterceptor, authAuthenticator: AuthAuthenticator): OkHttpClient =
	OkHttpClient.Builder()
		.connectTimeout(ConnectTimeout, TimeUnit.SECONDS)
		.writeTimeout(WriteTimeout, TimeUnit.SECONDS)
		.readTimeout(ReadTimeout, TimeUnit.SECONDS)
		.addInterceptor(
			HttpLoggingInterceptor().apply {
				level = HttpLoggingInterceptor.Level.BODY
			}
		)
		.addInterceptor(tokenInterceptor)
		.authenticator(authAuthenticator)
		.build()
profile
"비몽(Bemong)"이라는 앱을 개발 및 운영 중인 안드로이드 개발자입니다.

0개의 댓글