๐Ÿค– ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ Access Token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ (Feat. dataStore)

๊น€๋ฏผ์ฃผยท2023๋…„ 8์›” 16์ผ
8
post-thumbnail

๋“ค์–ด๊ฐ€๊ธฐ

๋“œ๋””์–ด ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ Access Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ์™€ Refresh Token ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•  ์ฐจ๋ก€๋‹ค!!
Access Token๊ณผ Refresh Token์ด ์–ด๋–ค ํ”„๋กœ์„ธ์Šค๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ์ž˜ ๋ชจ๋ฅด์‹œ๋Š” ๋ถ„๋“ค์€ ๐Ÿง Access Token๊ณผ Refresh Token์ด๋ž€ ๋ฌด์—‡์ด๊ณ  ์™œ ํ•„์š”ํ• ๊นŒ? โ† ์ด ๊ธ€์„ ๋จผ์ € ๋ณด๊ณ  ์˜ค๋Š” ๊ฒŒ ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค!

์ƒ๊ฐ๋ณด๋‹ค ๊ธ€์ด ๊ธธ์–ด์ ธ์„œ Access Token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ์™€ Refresh Token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ ๊ธ€์„ ๋‚˜๋ˆ ์„œ ์ž‘์„ฑํ–ˆ๋‹ค.

์˜ค๋Š˜์€ Access Token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ ๊ณผ์ • & ์ฝ”๋“œ๋ฅผ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ๋‹ค.

์ฐธ๊ณ ๋กœ ์•„๋ž˜ ์ฝ”๋“œ๋Š” ์ „๋ถ€ ์ฝ”ํ‹€๋ฆฐ์œผ๋กœ ๊ตฌํ˜„๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค!๐Ÿ™‚

๋ชฉํ‘œ

Access Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ

1๏ธโƒฃ Refresh Token์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ API ์žฌ์š”์ฒญํ•˜๊ธฐ
2๏ธโƒฃ ์ƒˆ Access Token ๋กœ์ปฌ(dataStore)์— ์ €์žฅํ•˜๊ธฐ

Overview

์œ„์˜ ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  ๊ธฐ์ˆ ๊ณผ ๊ตฌํ˜„ํ•  ์ž‘์—…์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • AuthInterceptor : OkHttp3์˜ Interceptor ์ƒ์†
    • request ํ—ค๋”์— JWT ํ† ํฐ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์ž‘์—…
    • response ํ—ค๋”์— Authorization ํ‚ค ๊ฐ’ ์กด์žฌํ•˜๋ฉด Access Token ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๋Š” ์ž‘์—…
  • AuthAuthenticator : OkHttp3์˜ Authenticator ์ƒ์†
    • 401 http error code๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ํด๋ž˜์Šค
    • Access Token ๋งŒ๋ฃŒ ์‹œ Access Token โžก Refresh Token์œผ๋กœ ๊ต์ฒด ํ›„ API ์žฌ์š”์ฒญํ•˜๋Š” ์ž‘์—…
  • TokenManager : Token DataStore < Preferences>
    • Access Token์„ ๋กœ์ปฌ์— ์ €์žฅํ•ด๋‘๋Š” ๊ณณ
    • ํ† ํฐ CRUD ์ž‘์—… ๋™๊ธฐ or ๋น„๋™๊ธฐ๋กœ ์„ ํƒํ•ด์„œ ์ž‘์—… ๊ฐ€๋Šฅ

OkHttp3

๋จผ์ € OkHttp์— ๋Œ€ํ•œ ์†Œ๊ฐœ๊ฐ€ ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค. OkHttp๋Š” ์•ฑ์—์„œ ๋„คํŠธ์›Œํฌ ํ†ต์‹ (์š”์ฒญ ๋ฐ ์‘๋‹ต)์„ ํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. HTTP๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜๋ฉด์„œ ๋กœ๋“œ๋ฅผ ๋น ๋ฅด๊ฒŒ ํ•˜๊ณ  ๋Œ€์—ญํญ์„ ์ ˆ์•ฝํ•˜๋Š” ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ OkHttp3๋Š” HTTP/2๋ฅผ ์ง€์›ํ•˜๊ณ  ์žˆ๋‹ค. HTTP/2๋Š” HTTP/1.1๊ณผ ์œ ์‚ฌํ•œ ๊ธฐ๋Šฅ์„ ํ•˜์ง€๋งŒ ๋” ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“  ํ”„๋กœํ† ์ฝœ์ด๋‹ค.

HTTP/2์˜ ํŠน์ง•

  • ์—ฌ๋Ÿฌ Request ํŒŒ์ผ์„ ๋ณ‘๋ ฌ์ ์œผ๋กœ ์ „์†กํ•œ๋‹ค
    • HTTP/1.1๊นŒ์ง€๋Š” request์— ๋Œ€ํ•œ response๊ฐ€ ์˜ค๊ณ  ๋‹ค์Œ request๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋ณ‘๋ชฉํ˜„์ƒ์ด๋ผ๋Š” ๋ฌธ์ œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ–ˆ๊ณ  ์ด๋ฅผ HOL(Head of line blocking)์ด๋ผ๊ณ  ํ•œ๋‹ค.
    • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด HTTP/2์—์„œ๋Š” ๋ณ‘๋ ฌ ์ „์†ก์„ ์ง€์›ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
  • ์ค‘๋ณต ํ—ค๋”์˜ ์ œ๊ฑฐ
    • ๊ฐ™์€ ๋‚ด์šฉ์˜ ํ—ค๋”๋ฅผ ๋ณด๋‚ผ ๊ฒฝ์šฐ ์ƒ๋žต ์ฒ˜๋ฆฌํ•˜์—ฌ ์†๋„๋ฅผ ๋†’์ธ๋‹ค
  • ํ—ค๋” ์••์ถ•
    • HTTP/1.1์—์„œ๋Š” ํ—ค๋”๋ฅผ ํ‰๋ฌธ์œผ๋กœ ๋ณด๋ƒˆ์ง€๋งŒ, HTTP/2์—์„œ๋Š” ํ—ค๋”๋ฅผ ์••์ถ•ํ•ด์„œ ๋ณด๋‚ด ์šฉ๋Ÿ‰ ๋Œ€๋น„ ์ฒ˜๋ฆฌ ํšจ์œจ์„ฑ์„ ๋†’์˜€๋‹ค.
  • ์šฐ์„ ์ˆœ์œ„
    • request์— ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‘์–ด ๋” ๋นจ๋ฆฌ ๋กœ๋”ฉ๋˜์–ด์•ผํ•˜๋Š” ํŒŒ์ผ์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค.

Frame

HTTP๋กœ ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ ์„ ํ•  ๋•Œ Frame์„ ๊ฐ€์ง€๊ณ  ํ•œ๋‹ค. Frame์€ Header์™€ Data๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ๋‹ค. Header์™€ Data์˜ ๊ตฌ์กฐ๋Š” ์•„๋ž˜ ์‚ฌ์ง„๊ณผ ๊ฐ™๋‹ค.


์šฐ๋ฆฌ๊ฐ€ ๊ด€์‹ฌ์„ ๊ฐ€์ ธ์•ผํ•˜๋Š” ๊ตฌ์กฐ๋Š” Header๋‹ค. ์•ž์„  ๋ธ”๋กœ๊ทธ์—์„œ ์ด์•ผ๊ธฐํ•œ ๊ฒƒ์ฒ˜๋Ÿผ JWT ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹์€ Header์— ์žˆ๋Š” JWT ํ† ํฐ์œผ๋กœ ์ธ๊ฐ€(Authorization)์„ ํ™•์ธํ•ด์ฃผ๋Š” ์ž‘์—…์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ด์ œ ๋‹ค์‹œ ๋Œ์•„๊ฐ€์„œ OkHttp3๊ฐ€ ์ง€์›ํ•ด์ฃผ๋Š” ํ—ค๋”์™€ ๊ด€๋ จ๋œ ํด๋ž˜์Šค๋ฅผ ๊ณต๋ถ€ํ•ด๋ณด์ž.

Interceptor

Interceptor๋Š” HTTP ํ†ต์‹ ์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๊ณ , API ์žฌ์š”์ฒญ ๋“ฑ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ํด๋ž˜์Šค์ด๋‹ค. Interceptor์—๋Š” Application Interceptor์™€ Network Interceptor 2๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

๐Ÿ“Œย Application Interceptor

  • ์ฃผ๋กœ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ด€๋ จ๋œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉ
  • ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ ์ด์ „์— ์‹คํ–‰ ๋จ
  • ์‚ฌ์šฉ ์˜ˆ) ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ ์ถ”๊ฐ€, ์บ์‹œ ๊ตฌํ˜„

๐Ÿ“Œย Network Interceptor

  • ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ฐ ์‘๋‹ต๊ณผ ๊ด€๋ จ๋œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ

  • ์‚ฌ์šฉ ์˜ˆ) ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ฐ ์‘๋‹ต ๋กœ๊ทธ

    ์šฐ๋ฆฌ๋Š” Authorization์„ ์ฒ˜๋ฆฌํ•  ๊ฒƒ์ด๋ฏ€๋กœ Application Interceptor๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

๐Ÿ”ฅ Access Token ๋งŒ๋ฃŒ ๊ตฌํ˜„ํ•˜๊ธฐ

Build

๋จผ์ € build.gradle(Module :app) ํŒŒ์ผ์— ์•„๋ž˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค. (๋งŒ๋ฃŒ ์ž‘์—…์„ ๊ตฌํ˜„ํ•˜๋Š” ์‚ฌ๋žŒ์ด๋ผ๋ฉด ์ด๋ฏธ DataStore๋ฅผ ์ œ์™ธํ•˜๊ณ ๋Š” ์ด๋ฏธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์„ ๊ฑฐ๋ผ ๋ฏฟ๋Š”๋‹ค!)

dependencies {
			// OkHttp
            implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
      		implementation("com.squareup.okhttp3:okhttp")
      		implementation("com.squareup.okhttp3:logging-interceptor")

			// DataStore
			implementation "androidx.datastore:datastore-preferences:1.0.0"

			//Retrofit
			def retrofit_version = "2.9.0"
			implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
			implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

			//Hilt
			def hilt_version = "2.44"
			implementation "com.google.dagger:hilt-android:$hilt_version"
			kapt "com.google.dagger:hilt-compiler:$hilt_version"
}

์ธํ„ฐ๋„ท ํ†ต์‹ ํ•˜๊ธฐ ์ „์— Internet ๊ถŒํ•œ๋„ AndroidManifest.xml์—์„œ ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋ง์ž.

<uses-permission android:name="android.permission.INTERNET" />

Token DataStore

JWT ํ† ํฐ์„ ๋กœ์ปฌ์— ์ €์žฅํ•ด์ฃผ๊ธฐ ์œ„ํ•ด DataStore Preference๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜์—ฌ ์ธ์ฆ(Authentication)ํ•˜๋ฉด ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ Refresh Token๊ณผ Access Token์„ ๋ฐ›๊ฒŒ ๋œ๋‹ค. ์ด๋•Œ TokenManager ์— ์ •์˜ํ•œ save_token()ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ Token์„ ์ €์žฅํ•˜๋ฉด ๋œ๋‹ค.

class TokenManager @Inject constructor(
    private val dataStore: DataStore<Preferences>
) {
    companion object {
        private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
    }

    fun getAccessToken(): Flow<String?> {
        return dataStore.data.map { prefs ->
            prefs[ACCESS_TOKEN_KEY]
        }
    }

    suspend fun saveAccessToken(token: String){
        dataStore.edit { prefs ->
            prefs[ACCESS_TOKEN_KEY] = token
        }
    }

    suspend fun deleteAccessToken(){
        dataStore.edit { prefs ->
            prefs.remove(ACCESS_TOKEN_KEY)
        }
    }
}

AuthInterceptor

OkHttp3์˜ Interceptor๋Š” request header์˜ ๋‚ด์šฉ ์ถ”๊ฐ€์™€ ๊ต์ฒด ๊ธฐ๋Šฅ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ์šฐ๋ฆฌ๋Š” JWT ํ† ํฐ์„ ํ—ค๋”์— ๋„ฃ์–ด์ค˜์•ผํ•˜๋ฏ€๋กœ Interceptor๋ฅผ ์ƒ์†๋ฐ›์€ AuthInterceptor ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค. Interceptor๋ฅผ ์ƒ์† ๋ฐ›์œผ๋ฉด interceptor() ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•˜๋ผ๊ณ  ๋œฌ๋‹ค.

๊ธฐ๋ณธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. (์•„๋ž˜์—์„œ ์ถ”๊ฐ€๋  ์˜ˆ์ •)

class AuthInterceptor @Inject constructor(
    private val tokenManager: TokenManager
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token: String = runBlocking {
            tokenManager.getAccessToken().first()
        } ?: return errorResponse(chain.request())

        val request = chain.request().newBuilder().header(AUTHORIZATION, "Bearer $token").build()

        return chain.proceed(request)
    }

    private fun errorResponse(request: Request): Response = Response.Builder()
        .request(request)
        .protocol(Protocol.HTTP_2)
        .code(NETWORK_ERROR)
        .message("")
        .body(ResponseBody.create(null, ""))
        .build()
}
  1. tokenManager ์—์„œ Access Token์„ ๋ฐ›์•„ ์˜จ๋‹ค.

    1-1. Access Token์ด null์ผ ๊ฒฝ์šฐ errorResponse๋ฅผ return ํ•ด์ค€๋‹ค.

    1-2. Access Token์— JWT ํ† ํฐ์ด ๋“ค์–ด์žˆ๋‹ค๋ฉด 2๋ฒˆ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.

  2. Interceptor๊ฐ€ ๊ฐ€์ ธ์˜จ chain์—์„œ request์— Builder๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

  3. ํ—ค๋”์˜ Authorization key์— Bearer๋ฅผ ๋ถ™์—ฌ Access Token ๊ฐ’์„ ๋„ฃ์–ด์ค€๋‹ค.

  4. chain.proceed(requset)๋ฅผ ํ†ตํ•ด HTTP ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต์„ ๋งŒ๋“ค๊ณ  return ํ•œ๋‹ค.

Access Token์„ ๋„ฃ์–ด ๋ณด๋ƒˆ๋˜ request์˜ Access Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด 401(Unauthorized) http ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค. ์•ž์„œ ๋งํ–ˆ๋“ฏ ์ด๋ฅผ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ํด๋ž˜์Šค๊ฐ€ Authenticator์ด๋‹ค. Authenticator๋Š” 401์„ ๋ฐ›์•˜์„ ๋•Œ๋งŒ ํ˜ธ์ถœ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

AuthAuthenticator

์ด์ œ AuthAuthenticator๋ฅผ ๊ตฌํ˜„ํ•˜์ž. Authenticator๋ฅผ ์ƒ์†๋ฐ›์•„ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๋ฉด authenticate() ํ•จ์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜๋ผ๊ณ  ๋œฌ๋‹ค. authenticate() ํ•จ์ˆ˜๋ฅผ ์ฑ„์›Œ๋ณด์ž.

๊ตฌํ˜„ํ•  ๋‚ด์šฉ์€ Access Token์„ Refresh Token์œผ๋กœ ๊ต์ฒดํ•˜์—ฌ API ์žฌ์š”์ฒญํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

class AuthAuthenticator @Inject constructor(
    private val tokenManager: TokenManager
) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val refreshToken = runBlocking {
            tokenManager.getRefreshToken().first()
        }
 
        if (refreshToken == null || refreshToken == "LOGIN") {
            response.close()
            return null
        }

        return newRequestWithToken(refreshToken, response.request)
    }

    private fun newRequestWithToken(token: String, request: Request): Request =
        request.newBuilder()
            .header(AUTHORIZATION, token)
            .build()
}
  1. Refresh Token์„ tokenManager์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ

    1-1. refreshToken์ด null์ด๊ฑฐ๋‚˜ โ€œLOGINโ€(๋กœ๊ทธ์ธ ์ „)์ด๋ผ๋ฉด API ์žฌ์š”์ฒญ request๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ๋‹ซ์•„๋ฒ„๋ฆฐ๋‹ค.

    1-2. refreshToken์— JWT ํ† ํฐ ๊ฐ’์ด ๋“ค์–ด์žˆ์œผ๋ฉด 2๋ฒˆ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.

  2. request ํ—ค๋”์˜ Authorization ๊ฐ’์„ Refresh Token์œผ๋กœ ๊ต์ฒดํ•˜๊ธฐ

  3. API ์žฌ์š”์ฒญ

์œ„ ์ฝ”๋“œ์—์„œ ์ฃผ๋ชฉํ•  ๋ถ€๋ถ„์€ runBlocking์ด๋‹ค. refreshToken์„ ๊ฐ€์ ธ์˜ฌ ๋•Œ runBlocking์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” ๋™๊ธฐ์ ์ธ ์ˆœ์ฐจ ํ๋ฆ„์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋‹ค. Refresh Token์„ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ์— request API๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฐธ๊ณ ๋กœ .first()๋ฅผ ํ•œ ์ด์œ ๋Š” dataStore์— ์ €์žฅ๋œ ํ† ํฐ์ด flow๋กœ ์ „๋‹ฌ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ํ† ํฐ์€ ํ•˜๋‚˜๋ฐ–์— ์—†์œผ๋ฏ€๋กœ stream์—์„œ ์ฒซ๋ฒˆ์งธ๋กœ ๋“ค์–ด์˜จ ๊ฐ’์ด ํ† ํฐ์ด ๋œ๋‹ค.

AuthInterceptor

์ด์ œ ๋‹ค์‹œ Interceptor์—์„œ ์ƒˆ๋กœ์šด response๋ฅผ ๋ฐ›์„ ์ฐจ๋ก€๋‹ค. ๋‚ด๊ฐ€ ๊ฐ€์ง„ Refresh Token์ด ์œ ํšจํ•œ ํ† ํฐ์ด๋ผ๋ฉด ์„œ๋ฒ„๋Š” ๋‚˜์—๊ฒŒ ์ƒˆ๋กœ์šด Access Token์„ ์ „๋‹ฌํ•ด์ค„ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

class AuthInterceptor @Inject constructor(
    private val tokenManager: TokenManager
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token: String = runBlocking {
            tokenManager.getAccessToken().first()
        } ?: return errorResponse(chain.request())

        val request = chain.request().newBuilder().header(AUTHORIZATION, "Bearer $token").build()
        
				////////////////////////////////// ์—ฌ๊ธฐ๋ถ€ํ„ฐ ์ถ”๊ฐ€๋œ ์ฝ”๋“œ /////////////////////////////////////
		val response = chain.proceed(request)
		if (response.code == HTTP_OK) {
        	val newAccessToken: String = response.header(AUTHORIZATION, null) ?: return response
        	Timber.d("new Access Token = ${newAccessToken}")

        	CoroutineScope(Dispatchers.IO).launch {
            	val existedAccessToken = tokenManager.getAccessToken().first()
            	if (existedAccessToken != newAccessToken) {
                	tokenManager.saveAccessToken(newAccessToken)
                	Timber.d("newAccessToken = ${newAccessToken}\nExistedAccessToken = ${existedAccessToken}")
            	}
        	}
        } else {
            Timber.e("${response.code} : ${response.request} \n ${response.message}")
        }
	
        return response
    }

    ...
}
  1. response code๊ฐ€ HTTP_OK(200)์ธ์ง€ ํ™•์ธํ•œ๋‹ค

  2. response ํ—ค๋”์— Authorization ํ‚ค ๊ฐ’์ด ์กด์žฌํ•˜๋Š” ์ง€ ํ™•์ธ

    2-1. ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด Refresh Token์œผ๋กœ request๋ฅผ ๋ณด๋‚ธ ๊ฒŒ ์•„๋‹ˆ๋ฏ€๋กœ ํ† ํฐ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”์—†์œผ๋‹ˆ return response๋ฅผ ํ•˜๊ณ  ํ•จ์ˆ˜๋ฅผ ์ข…๋ฃŒํ•œ๋‹ค

    2-2. ์กด์žฌํ•œ๋‹ค๋ฉด 3๋ฒˆ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.

  3. Authorization์— ๋“ค์–ด์žˆ๋Š” ์ƒˆ๋กœ์šด Access Token๊ณผ tokenManager์— ๋“ค์–ด์žˆ๋Š” Access Token์„ ๋น„๊ตํ•ด์ค€๋‹ค.

    3-1. ๋™์ผํ•˜๋‹ค๋ฉด ์•ž์„  response์—์„œ ์ด๋ฏธ ์—…๋ฐ์ดํŠธํ•ด์ค€ ๊ฒƒ์ด๋ฏ€๋กœ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•ด์ค„ ํ•„์š” ์—†๋‹ค.

    3-2. ๋™์ผํ•˜์ง€ ์•Š๋‹ค๋ฉด ์ƒˆ๋กœ์šด Access Token์ด๋ฏ€๋กœ ์ €์žฅ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.


๐Ÿ‘€ ์—ฌ๊ธฐ์„œ ์งˆ๋ฌธ!

Q. ์ƒˆ๋กœ์šด Access Token์„ ์ €์žฅํ•  ๋•Œ ๋น„๋™๊ธฐ(CoroutineScope(Dispatchers.IO))์—์„œ ์ž‘์—…ํ•œ ์ด์œ ๋Š”?

A. UI๋ฅผ blockingํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ 2~3๋ฒˆ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ๋” ๋‚ซ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๋ฌผ๋ก  ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋ณ‘๋ ฌ์ ์œผ๋กœ response๊ฐ€ ์˜ค๋ฏ€๋กœ (HTTP/2์˜ ํŠน์„ฑ) ํ•ด๋‹น ์ž‘์—…์ด 1๋ฒˆ ์ด์ƒ ์ˆ˜ํ–‰๋  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฏธ ๋ฐ›์€ response์ด๋ฏ€๋กœ ๋น„๋™๊ธฐ๋กœ ์ž‘์—…ํ•œ๋‹ค๊ณ  ํ•ด์„œ ๋„คํŠธ์›Œํฌ์— ํ˜ผ์„ ์ด๋‚˜ ๋ถ€ํ•˜๋ฅผ ์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ runBlocking์„ ์‚ฌ์šฉํ•˜์—ฌ UI๋ฅผ blockingํ•˜๋ฉด์„œ๊นŒ์ง€ ๋™๊ธฐ์ ์œผ๋กœ ์ž‘์—…ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๊ณ  ๋Š๊ผˆ๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

๊ฒฐ๋ก 

  • Access Token์˜ ํ๋ฆ„
    1. AccessToken์„ DataStore์—์„œ ๊ฐ€์ ธ์™€ ํ—ค๋”์— ๋„ฃ์–ด์ฃผ๊ธฐ
    2. ๋งŒ๋ฃŒ๋˜์–ด 401 ๋ฐ›์œผ๋ฉด Authenticator์—์„œ Access Token โ†’ Refresh Token์œผ๋กœ ๋Œ€์ฒดํ•˜์—ฌ API request ์žฌ์š”์ฒญ
    3. response ํ—ค๋”์˜ Authorization์— Access Token ๋“ค์–ด์žˆ์œผ๋ฉด DataStore์˜ Access Token ์—…๋ฐ์ดํŠธ
  • Access Token ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์™€ ๋ชฉ์ 
    - AuthInterceptor : request ํ—ค๋”์— Access Token ์ถ”๊ฐ€ํ•ด์ฃผ๊ณ , ์ƒˆ Access Token ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด
    - AuthAuthenticator : 401(Unauthorized) ์ƒํƒœ์ฝ”๋“œ ๋ฐ›์•˜์„ ๋•Œ Access Token โ†’ Refresh Token์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์ž‘์—… ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด
    - TokenManager : Access Token CRUD ์ž‘์—… ๋น„๋™๊ธฐ & ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด


๊ณ ๋ฏผ ๊ธฐ๋ก 1๏ธโƒฃ : DataStore๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ 

๊ธฐ์กด์—๋Š” ์‚ฌ์‹ค DataStore ๋Œ€์‹ ์— SharedPreference๋ฅผ ์‚ฌ์šฉํ–ˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์œ ๋กœ DataStore๋ฅผ ์‚ฌ์šฉํ•ด JWT ํ† ํฐ์„ ์ €์žฅํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

1. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
SharedPreference์™€ ๋‹ฌ๋ฆฌ DataStore๋Š” Flow๋กœ ๋ฐ์ดํ„ฐ๋ฅผ stream์— ์ €์žฅํ•œ๋‹ค. ๋”ฐ๋ผ์„œ CoroutineScope ๋‚ด์—์„œ ๋น„๋™๊ธฐ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ–ˆ๋‹ค. UI๋ฅผ ๋ง‰์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด ๊ฐ€์žฅ ๋งˆ์Œ์— ๋“ค์—ˆ๋‹ค.

2. ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ด๋‹ค.
์•„์ง ์œ„์˜ ์ฝ”๋“œ์—์„œ ๋‚˜์˜ค์ง€๋Š” ์•Š์•˜์ง€๋งŒ ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๋กœ์ปฌ์— ์ €์žฅ๋œ JWT ํ† ํฐ์„ ๋‹ค ์ง€์›Œ์ฃผ์–ด์•ผํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ SharedPreference๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI๋ฅผ ๋ง‰๊ฑฐ๋‚˜ (๋กœ๊ทธ์ธ ์ด๋™ ํ™”๋ฉด ์ด๋™ ์ž‘์—… ์ „์— ์ด๋ฃจ์–ด์ ธ์•ผํ•˜๋ฏ€๋กœ) ์ œ๋Œ€๋กœ ์•ˆ์ง€์›Œ์ง€๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ฒผ๋‹ค. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋•๋ถ„์— ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ด๊ฒŒ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„ ๊ฒƒ์ด๋‹ค!

๊ณ ๋ฏผ ๊ธฐ๋ก 2๏ธโƒฃ : Interceptor์™€ Authenticator 2๊ฐœ๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ 

์‚ฌ์‹ค Interceptor๋งŒ ์‚ฌ์šฉํ•ด์„œ๋„ Access Token ๋งŒ๋ฃŒ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์œ„์—์„œ HTTP_OK๋ฅผ ํ™•์ธํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ์—๋Ÿฌ ์ฝ”๋“œ๊ฐ€ UNAUTHORIZED์ธ์ง€ ํ™•์ธํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Refresh Token ๋งŒ๋ฃŒ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์–ด๋ ค์›€์„ ๊ฒช์—ˆ๊ณ  2๊ฐœ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ•œ ๊ฒƒ์ด๋‹ค.

์ด ๋ถ€๋ถ„์€ ๋‹ค์Œ ์ฑ•ํ„ฐ์—์„œ ์ž์„ธํžˆ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ๋‹ค.

๊ทธ๋Ÿผ ์•ˆ๋…•ํžˆ!!!๐Ÿ‘‹

profile
์ฆ๊ฑฐ์šด ๊ฐœ๋ฐœ์ž ๊น€๋ฏผ์ฃผ์ž…๋‹ˆ๋‹ค๐Ÿ™‚

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2024๋…„ 5์›” 3์ผ

์ •๋ง ๋ฉ‹์ง€์„ธ์š”

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ