[Android, Kotlin] Interceptor + Refresh 토큰을 활용한 재인증 구현 2

아래 방식도 오류가 발생해서 최종 코드로 리포스팅 했습니다.

https://jinudmjournal.tistory.com/89

 

[Android, Kotlin] Interceptor + Refresh 토큰을 활용한 재인증 구현 3 - 문제 해결, 최종 코드

[문제 상황] 기존에 Interceptor와 Refresh 토큰을 활용한 재인증 코드를 작성했는데, 포스팅 1에서 TokenAuthenticator를 활용한 토큰 인증을 구현했다. https://jinudmjournal.tistory.com/86 [Android, Kotlin] Interceptor +

jinudmjournal.tistory.com

 

 

 

이전에 작성한 포스팅에서 오류가 발생해서 리포스팅 합니다.

 

인증을 진행하는 TokenAuthenticator.kt 클래스에서 오류가 발생했다.

header를 재정의하고 테스팅을 진행하였다.

 

[MasterApplication.kt]

 

- 토큰 인증을 적용하고 새로 레트로핏을 생성한다.

- 삭제된 부분은 주석처리 했다.

 

createRetrofit : 액티비티를 받아서 레트로핏을 생성하는 함수이다.

    fun createRetrofit(currentActivity: Activity){
        activity = currentActivity

        val header = Interceptor{
            val original = it.request()
            if (checkIsLogin()){
                getUserToken().let { token->
                    Log.d("token test 1",token.toString())
                    val request = original.newBuilder()
                        .header("Authorization","Bearer $token")
                        .build()
                    it.proceed(request)
                }
            }else{
                it.proceed(original)
            }
        }

//        val normalClient = OkHttpClient.Builder()
//            .addInterceptor(header)
//            .addInterceptor(logInterceptor)
//            .addNetworkInterceptor(StethoInterceptor())
//            .build()
//
//
//        val client = OkHttpClient.Builder()
//            .addInterceptor(header)
//            .addInterceptor(AuthInterceptor(activity,buildTokenApi(normalClient)))
//            .build()
//

        val retrofit = Retrofit.Builder()
            .baseUrl("$baseUrl/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .client(getRetrofitClient(header))
            .build()

        service = retrofit.create(RetrofitService::class.java)

//        //토큰을 위한 인증
//        //TokenAuthenticator으로 새 토큰 발행
//        val authenticator = TokenAuthenticator(activity,buildTokenApi())
//
//        //레트로핏 생성
//        val retrofit = Retrofit.Builder()
//            .baseUrl("$baseUrl/")
//            .client(getRetrofitClient(authenticator))
//            .addConverterFactory(GsonConverterFactory.create())
//            .build()
//            .create(RetrofitService::class.java)
//        service = retrofit

    }

 

기존 레트로핏 통신과 같이 header에 인증 정보로 토큰을 담는다.

이 때 로그인 시 저장된 토큰이 있는지 확인하는 checkIsLogin() 함수를 실행한다.

토큰이 존재하다면 getUserToken으로 저장 된 acccessToken을 불러온다.

private fun checkIsLogin() : Boolean{
    val sp = activity.getSharedPreferences("login_sp",Context.MODE_PRIVATE)
    val token = sp.getString("accessToken","null")
    return token!="null"
    //is not default
}
private fun getUserToken(): String?{
    val sp = activity.getSharedPreferences("login_sp",Context.MODE_PRIVATE)
    val token = sp.getString("accessToken","null")
    return if (token=="null") null
    else token
}

 

 

레트로핏을 생성할 때 Client를 getRetrofitClient 함수를 통해 전달 받는다.

이 함수는 createRetrofit에서 생성한 토큰을 전달하는 Interceptor(val header 변수)를 인자로 받는다.

이 함수는 클라이언트를 빌드하는데 3가지 인터셉터를 등록한다.

1.  Interceptor(val header 변수) : access토큰을 사용한 인증 인터셉터

2. AuthIntercepter : 400,401,404 에러 등이 발생할 때 처리 인터셉터

3. logIntercepter : 개발자가 서버에서 전달받는 내용을 Log 기록으로 볼 수 있도록 해주는 인터셉터

 

1,2 번은 토큰 관련 인터셉터이고, 3번은 오류 확인을 위해서 추가로 등록했다.

 

private fun getRetrofitClient(header:Interceptor):OkHttpClient{
    return OkHttpClient.Builder()
        .addInterceptor {chain->
            chain.proceed(chain.request().newBuilder().also {
                it.addHeader("Accept", "application/json")
            }.build())
        }.also { client->
            client.addInterceptor(header)
            client.addInterceptor(AuthInterceptor(activity,buildTokenApi(client.build())))
            //오류 관련 인터셉터도 등록 (오류 출력 기능 )
            val logInterceptor = HttpLoggingInterceptor()
            logInterceptor.level = HttpLoggingInterceptor.Level.BODY
            client.addInterceptor(logInterceptor)
        }.build()
}

 

AuthIntercepter는 아래와 같은 구조를 가지며 2가지 parm을 받는다.

class AuthInterceptor(
    private val context: Context,
    private val tokenApi: TokenRefreshApi,
    ) : Interceptor,BaseRepository()

context는 activityContext이며, tokenApi는 인터페이스이다.

//TokenRefreshApi를 빌드
private fun buildTokenApi(client: OkHttpClient) : TokenRefreshApi {
    return Retrofit.Builder()
        .baseUrl("$baseUrl/")
        .client(client)
        .addConverterFactory(ScalarsConverterFactory.create())
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(TokenRefreshApi::class.java)
}

bulidTokenApi 함수를 통해서 TokenRefreshApi를 리턴 받는다.

해당 APi는 토큰 호출을 위한 인터페이스로 서버로부터 새로운 토큰을 받는 기능을 한다.

아직 레트로핏이 생성되기 전에 서버와 통신을 해야하므로 토큰을 받기 위한 임시 레트로핏이라고 보면 된다.

//Refresh Api 호출을 위한 인터페이스
// 새로운  access , refresh token 반환
interface TokenRefreshApi {
    @PATCH("token")
    suspend fun patchToken(
        @Body refreshToken:String
    ) : Token
}

 

AuthIntercepter의 내부 구조는 아래와 같다.

class AuthInterceptor(
    private val context: Context,
    private val tokenApi: TokenRefreshApi,
    ) : Interceptor,BaseRepository(){
    override fun intercept(chain: Interceptor.Chain): Response{
        val request = chain.request()
        val response = chain.proceed(request)

        when( response.code){
            400 ->{

            }
            401 ->{
                // 토큰 재인증
                runBlocking {
                    when (val token = getUpdateToken()) {
                        is Resource.Success -> {
                            val sp  =context.getSharedPreferences("login_sp",Context.MODE_PRIVATE)
                            val editor = sp.edit()
                            // 기존 토큰
                            Log.d("test code ists sssss 2",sp.getString("accessToken","no token").toString())
                            val tokenValue = token.value!!
                            // new 토큰
                            Log.d("test code ists sssss 3",token.value.accessToken)
                            val accessToken = tokenValue.accessToken
                            val refreshToken =tokenValue.refreshToken
                            editor.putString("accessToken", accessToken)
                            editor.putString("refreshToken", refreshToken)
                            editor.apply()
                            response.request.newBuilder()
                                .header("Authorization","Bearer $accessToken")
                                .build()
                        }
                        else ->{}
                    }
                    response
                }
            }
            403 ->{

            }
            404 ->{

            }
        }
        return response
    }

    private suspend fun getUpdateToken() : Resource<Token?> {
        val refreshToken = context.getSharedPreferences("login_sp",Context.MODE_PRIVATE)
            .getString("refreshToken","").toString()

        // updateToken 확인
        Log.d("test code ists sssss 4 ",refreshToken.toString())

        //safeApiCall을 통한 api 요청
        // refresh token이 비었을 경우에는 null 전송을 통해서 에러 반환을 받음
        return safeApiCall {tokenApi.patchToken(refreshToken)}
    }


}

 

위 인터셉터는 통신 오류에 따른 처리를 해준다.

401에러는 토큰 인증에러이며, 정상적으로 작동하던 코드가 401에러가 발생했다면 토큰이 만료되었다는 것이다.

이 때 새로운 토큰을 발행하고 갱신하는 부분을 담당한다.

//Resource : 통신의 에러를 구분
// 성공적인 통신일 경우 Success
// 실패한 경우 Failure을 통한 에러 조사
sealed class Resource<out T> {
    // 통신 성공
    data class Success<out T>(val value: T) : Resource<T>()

    //통신 실패 데이터
    data class Failure(
        val isNetworkError: Boolean,
        val errorCode: Int?,
        val errorBody: ResponseBody?
    ) : Resource<Nothing>()
}
//안전한 api 전송을 위한 코드
// Resource.Success(apiCall.invoke()) 코드를 반환하는 것이 목표
// 실패할 경우 에러 반환
interface SafeApiCall {

    suspend fun <T> safeApiCall(
        apiCall: suspend () -> T
    ): Resource<T> {
        return withContext(Dispatchers.IO) {
            try {
                // 통신에 성공하면 전달 받은 값 전송
                Resource.Success(apiCall.invoke())
            } catch (throwable: Throwable) {
                when (throwable) {
                    is HttpException -> {
                        Resource.Failure(false, throwable.code(), throwable.response()?.errorBody())
                    }
                    else -> {
                        Resource.Failure(true, null, null)
                    }
                }
            }
        }
    }
}

getUpdateToken 함수를 통해서 서버로부터 성공적으로 토큰이 오면 새로 토큰을 갱신하는 코드를 작성했다.

response.request.newBuilder()
    .header("Authorization","Bearer $accessToken")
    .build()

위 코드를 통해서 새로 access토큰이 저장된다.

 

로그인 동작을 통해서 메인 액티비티로 이동하면 서버와 통신하는 서비스를 이용한다.

//MaterApplication : 로그인 + API 연동////
val masterApp = MasterApplication()
///////////////////////////////////////

masterApplication을 생성하고,

//masterApp init //////////////
masterApp.createRetrofit(this@MainActivity)
//////////////////////

onCreate 내부에서 createRetrofit 해주면 된다.

 

이제 서버와 통신할 때 아래의 방식대로 동작한다.

 

토큰과 함께 API 연결

if> 통신 성공  -> 응답 받음 

else> 통신 실패 (401에러) -> Authinterceptor가 먼저 통신을 가로채서 토큰 재인증 -> 토큰 갱신 -> 응답 받음