Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

리프레시 토큰 재발급 401 대응 #68

Merged
merged 10 commits into from
Oct 28, 2024
Merged
1 change: 1 addition & 0 deletions core/data/auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ dependencies {
implementation(project(":core:domain:auth"))
implementation(project(":core:network"))
implementation(project(":core:datastore"))
implementation(project(":core:navigation"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.goalpanzi.mission_mate.core.data.auth

import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource
import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSource
import com.goalpanzi.mission_mate.core.navigation.NavigationEventHandler
import com.goalpanzi.mission_mate.core.navigation.di.AuthNavigation
import com.goalpanzi.mission_mate.core.network.TokenExpirationHandler
import kotlinx.coroutines.flow.collect
import javax.inject.Inject

class AuthTokenExpirationHandler @Inject constructor(
private val authDataSource: AuthDataSource,
private val defaultDataSource: DefaultDataSource,
private val missionDataSource: MissionDataSource,
@AuthNavigation private val authNavigationEventHandler: NavigationEventHandler
) : TokenExpirationHandler {

override suspend fun handleRefreshTokenExpiration() {
clearCachedData()
authNavigationEventHandler.triggerRouteToNavigate("RouteModel.Login")
}

private suspend fun clearCachedData(){
authDataSource.setAccessToken("")
authDataSource.setRefreshToken("")
defaultDataSource.clearUserData().collect()
missionDataSource.clearMissionData().collect()
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.goalpanzi.mission_mate.core.data.auth.di

import com.goalpanzi.mission_mate.core.data.auth.AuthTokenExpirationHandler
import com.goalpanzi.mission_mate.core.data.auth.AuthTokenProvider
import com.goalpanzi.mission_mate.core.data.auth.repository.AuthRepositoryImpl
import com.goalpanzi.mission_mate.core.domain.auth.repository.AuthRepository
import com.goalpanzi.mission_mate.core.network.TokenExpirationHandler
import com.goalpanzi.mission_mate.core.network.TokenProvider
import dagger.Binds
import dagger.Module
Expand All @@ -18,4 +20,7 @@ internal abstract class AuthDataModule {

@Binds
abstract fun bindTokenProvider(impl : AuthTokenProvider) : TokenProvider

@Binds
abstract fun bindTokenExpirationHandler(impl : AuthTokenExpirationHandler) : TokenExpirationHandler
}
7 changes: 6 additions & 1 deletion core/navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.kotlin.plugin.serialization)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.hilt.android)
}

android {
Expand Down Expand Up @@ -37,4 +39,7 @@ android {

dependencies {
implementation(libs.kotlin.serialization.json)
}
implementation(libs.bundles.coroutines)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.goalpanzi.mission_mate.core.navigation

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject

class AuthNavigationEventHandler @Inject constructor() : NavigationEventHandler {

private val _routeToNavigate = MutableSharedFlow<String>()
override val routeToNavigate: SharedFlow<String> = _routeToNavigate.asSharedFlow()

override suspend fun triggerRouteToNavigate(route: String) {
_routeToNavigate.emit(route)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.goalpanzi.mission_mate.core.navigation

import kotlinx.coroutines.flow.SharedFlow

interface NavigationEventHandler {
val routeToNavigate : SharedFlow<String>

suspend fun triggerRouteToNavigate(route : String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.goalpanzi.mission_mate.core.navigation.di

import com.goalpanzi.mission_mate.core.navigation.AuthNavigationEventHandler
import com.goalpanzi.mission_mate.core.navigation.NavigationEventHandler
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Qualifier
import javax.inject.Singleton

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class AuthNavigation

@Module
@InstallIn(SingletonComponent::class)
abstract class NavigationModule {

@AuthNavigation
@Binds
@Singleton
abstract fun bindAuthNavigationEventHandler(
impl : AuthNavigationEventHandler
) : NavigationEventHandler
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.goalpanzi.mission_mate.core.network

interface TokenExpirationHandler {
suspend fun handleRefreshTokenExpiration()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.goalpanzi.mission_mate.core.network.di

import com.goalpanzi.mission_mate.core.network.BuildConfig
import com.goalpanzi.mission_mate.core.network.TokenInterceptor
import com.goalpanzi.mission_mate.core.network.interceptor.TokenInterceptor
import com.goalpanzi.mission_mate.core.network.interceptor.TokenReissueInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -13,8 +14,25 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import javax.inject.Qualifier
import javax.inject.Singleton

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class TokenInterceptorHttpClient

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class TokenReissueInterceptorHttpClient

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class TokenRetrofit

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class TokenReissueRetrofit

@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
Expand All @@ -25,19 +43,32 @@ internal object NetworkModule {
return HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
}

@TokenInterceptorHttpClient
@Provides
@Singleton
fun provideOkhttpClient(
fun provideTokenInterceptorHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
tokenInterceptor: TokenInterceptor
): OkHttpClient {

return OkHttpClient.Builder()
.addInterceptor(tokenInterceptor)
.addInterceptor(httpLoggingInterceptor)
.build()
}

@TokenReissueInterceptorHttpClient
@Provides
@Singleton
fun provideTokenReissueInterceptorHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
tokenReissueInterceptor: TokenReissueInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(tokenReissueInterceptor)
.addInterceptor(httpLoggingInterceptor)
.build()
}

@Provides
@Singleton
fun provideJson(): Json = Json {
Expand All @@ -53,10 +84,25 @@ internal object NetworkModule {
return json.asConverterFactory("application/json".toMediaType())
}

@TokenRetrofit
@Provides
@Singleton
fun provideTokenRetrofit(
@TokenInterceptorHttpClient okHttpClient: OkHttpClient,
converterFactory: Converter.Factory
) : Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(converterFactory)
.build()
}

@TokenReissueRetrofit
@Provides
@Singleton
fun provideRetrofit(
okHttpClient: OkHttpClient,
fun provideTokenReissueRetrofit(
@TokenReissueInterceptorHttpClient okHttpClient: OkHttpClient,
converterFactory: Converter.Factory
) : Retrofit {
return Retrofit.Builder()
Expand All @@ -65,4 +111,4 @@ internal object NetworkModule {
.addConverterFactory(converterFactory)
.build()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.goalpanzi.mission_mate.core.network.di

import com.goalpanzi.mission_mate.core.network.BuildConfig
import com.goalpanzi.mission_mate.core.network.TokenProvider
import com.goalpanzi.mission_mate.core.network.service.LoginService
import com.goalpanzi.mission_mate.core.network.service.MissionService
import com.goalpanzi.mission_mate.core.network.service.OnboardingService
Expand All @@ -11,11 +9,6 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import javax.inject.Singleton

Expand All @@ -25,56 +18,41 @@ object ServiceModule {

@Provides
@Singleton
fun provideLoginService(retrofit: Retrofit): LoginService {
fun provideLoginService(
@TokenRetrofit retrofit: Retrofit
): LoginService {
return retrofit.create(LoginService::class.java)
}

@Provides
@Singleton
fun provideProfileService(retrofit: Retrofit): ProfileService {
fun provideProfileService(
@TokenRetrofit retrofit: Retrofit
): ProfileService {
return retrofit.create(ProfileService::class.java)
}

@Provides
@Singleton
fun provideOnboardingService(retrofit: Retrofit): OnboardingService {
fun provideOnboardingService(
@TokenRetrofit retrofit: Retrofit
): OnboardingService {
return retrofit.create(OnboardingService::class.java)
}

@Provides
@Singleton
fun provideMissionService(retrofit: Retrofit): MissionService {
fun provideMissionService(
@TokenRetrofit retrofit: Retrofit)
: MissionService {
return retrofit.create(MissionService::class.java)
}

@Provides
@Singleton
fun provideTokenService(
httpLoggingInterceptor: HttpLoggingInterceptor,
converterFactory: Converter.Factory,
tokenProvider: TokenProvider
): TokenService {
val tokenReissueInterceptor = Interceptor { chain ->
val newRequest = chain.request().newBuilder().apply {
runBlocking {
val token = tokenProvider.getAccessToken()
token?.let {
addHeader("Authorization", "Bearer $it")
}
}
}
chain.proceed(newRequest.build())
}
val retrofit = Retrofit.Builder()
.client(
OkHttpClient.Builder()
.addInterceptor(tokenReissueInterceptor)
.addInterceptor(httpLoggingInterceptor)
.build()
)
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(converterFactory)
.build()
@TokenReissueRetrofit retrofit: Retrofit
) : TokenService {
return retrofit.create(TokenService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.goalpanzi.mission_mate.core.network
package com.goalpanzi.mission_mate.core.network.interceptor

import com.goalpanzi.mission_mate.core.network.TokenProvider
import com.goalpanzi.mission_mate.core.network.model.request.TokenReissueRequest
import com.goalpanzi.mission_mate.core.network.service.TokenService
import kotlinx.coroutines.CoroutineScope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.goalpanzi.mission_mate.core.network.interceptor

import com.goalpanzi.mission_mate.core.network.TokenExpirationHandler
import com.goalpanzi.mission_mate.core.network.TokenProvider
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import java.net.HttpURLConnection
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TokenReissueInterceptor @Inject constructor(
private val tokenProvider: TokenProvider,
private val tokenExpirationHandler : TokenExpirationHandler
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().newBuilder().apply {
runBlocking {
val token = tokenProvider.getAccessToken()
token?.let {
addHeader("Authorization", "Bearer $it")
}
}
}
val response = chain.proceed(newRequest.build())

when (response.code) {
HttpURLConnection.HTTP_UNAUTHORIZED -> {
runBlocking {
tokenExpirationHandler.handleRefreshTokenExpiration()
}
return response
}
}
return response
}
}
Loading
Loading