okhttp3를 이용하여 재인증하기(refresh token)

2021. 3. 18. 02:00 안드로이드/개발 TIP

앱에서 서버로 인증할때는 OAuth 2.0방법을 가장많이 사용하는데 이에 대한 설명은 아래의 링크를 참조해주세요.

https://oauth.net/2/

 

OAuth 2.0에서는  JWT(Json Web Token)를 이용합니다.

이는 인증정보를 암호화하여 url형식으로 전달해 주는 토큰이라고 합니다.

Token이라는 것은 만료시간이 있으며 만료된 Token은 더이상 사용하지 못합니다.

만약 만료된 Token으로 서버와 통신한다면 401 error를 만나게 됩니다.

따라서 만료된 access token은 refresh token으로 새로운 access token을 갱신받아 사용해야만 합니다.

 

그럼 안드로이드에서는 토큰 갱신을 어떻게 구현할까요?

바로 okhttp3를 통해 쉽게 구현할 수 있습니다.

val builder =  OkHttpClient.Builder()
	.connectTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
	.readTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
	.writeTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
	.authenticator(authenticator)

빌드에서 authenticator를 추가만 하면 됩니다.

그럼 authenticator는 어떻게 구현할까요? 아래의 예제가 있습니다.

class TokenAuthenticator constructor(
    val context: Context,
    private val refreshToken: String,
) : Authenticator {

	companion object {
        private val TAG = TokenAuthenticator::class.java.simpleName
    }

    override fun authenticate(route: Route?, response: Response): Request? {
   
        if (response.code == 401) {

            val refreshToken = CommonHelper.getRefreshToken(sharedPref)
            val getNewDeviceToken = GlobalScope.async(Dispatchers.Default) {
                getNewDeviceToken(refreshToken)
            }

            val token = runBlocking {
                getNewDeviceToken.await()
            }
            if(token != null) {
                return getRequest(response, token)
            } 
        }
        return null
    }

    private suspend inline fun getNewDeviceToken(token: String): String? {
        return GlobalScope.async(Dispatchers.Default) {
            callApiNewDeviceToken(token)
        }.await()
    }


    private suspend inline fun callApiNewDeviceToken(token: String) : String? = suspendCoroutine { continuation ->
        createWebService<Api>()
            .refreshToken(RefreshToken(token))
            .with(rx)
            .response(object : ApiCallback<Token>{
                override fun success(data: Token?) {
                    if(data != null) {
                        CommonHelper.saveTokenInfo(sharedPref, data)
                        continuation.resume(data.accessToken)
                    } else {
                        continuation.resume(null)
                    }
                }

                override fun error(statusCode: Int, message: String?) {
                    continuation.resume(null)
                }
            })

        return@suspendCoroutine
    }

    private val okHttp =  OkHttpClient.Builder()
        .connectTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
        .readTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
        .writeTimeout(TIMEOUT_LIMIT, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) {
                HttpLoggingInterceptor.Level.BODY
            } else {
                HttpLoggingInterceptor.Level.NONE
            }
        })
        .build()

    private inline fun <reified T> createWebService(): T {
        val retrofit = Retrofit.Builder()
            .baseUrl(BuildConfig.SERVER_URL)
            .client(okHttp)
            .addConverterFactory(GsonConverterFactory.create(
                GsonBuilder().serializeNulls().create()
            ))
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
        return retrofit.create(T::class.java)
    }

    private fun getRequest(response: Response, token: String): Request {
        return response.request
            .newBuilder()
            .removeHeader("Authorization")
            .addHeader("Authorization", "Bearer $token")
            .build()
    }
}

토큰을 체크하고 새로운 토큰을 발급받아 교체만 해주면 됩니다.

여기서 주의할점은 토큰 재발급시 새로운 okhttp의 빌더를 만들고나서 authenticator는 추가하지 말아야합니다.

토큰을 발급받는 api마져 401 error를 받게 된다면 무한루프로 stackoverflow가 발생할 여지가 있기 때문입니다.

 

출처 : akaisun.tistory.com/73