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

[SJ-#25] 자동 로그인 및 토큰 재발급 설정 #58

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/java/com/example/koview/app/di/ApiModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.koview.app.di

import com.example.koview.data.remote.AuthApi
import com.example.koview.data.remote.IntroApi
import com.example.koview.data.remote.MainApi
import dagger.Module
Expand All @@ -13,6 +14,10 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object ApiModule {

@Singleton
@Provides
fun provideAuthApi(retrofit: Retrofit): AuthApi = retrofit.create(AuthApi::class.java)

@Singleton
@Provides
fun provideIntroApi(retrofit: Retrofit): IntroApi = retrofit.create(IntroApi::class.java)
Expand Down
14 changes: 8 additions & 6 deletions app/src/main/java/com/example/koview/app/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.example.koview.app.di

import com.example.koview.BuildConfig
import com.example.koview.data.config.AccessTokenInterceptor
import com.example.koview.data.config.BearerInterceptor
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -15,12 +16,10 @@ import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
object NetworkModule {

@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {

return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_DEV_URL)
.client(okHttpClient)
Expand All @@ -29,24 +28,27 @@ class NetworkModule {
}

@Provides
@Singleton
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}

@Singleton
@Provides
fun provideBearerInterceptor(): BearerInterceptor = BearerInterceptor()

@Provides
fun provideOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor
httpLoggingInterceptor: HttpLoggingInterceptor,
bearerInterceptor: BearerInterceptor
): OkHttpClient {

return OkHttpClient.Builder()
.readTimeout(30000, TimeUnit.MILLISECONDS)
.connectTimeout(30000, TimeUnit.MILLISECONDS)
.addInterceptor(httpLoggingInterceptor)
.addNetworkInterceptor(AccessTokenInterceptor())
.addInterceptor(bearerInterceptor)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.koview.app.di

import com.example.koview.data.repository.AuthRepository
import com.example.koview.data.repository.AuthRepositoryImpl
import com.example.koview.data.repository.IntroRepository
import com.example.koview.data.repository.IntroRepositoryImpl
import com.example.koview.data.repository.MainRepository
Expand All @@ -13,6 +15,9 @@ import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

@Binds
abstract fun bindAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository

@Binds
abstract fun bindIntroRepository(introRepositoryImpl: IntroRepositoryImpl): IntroRepository

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.example.koview.data.config

import android.util.Log
import com.example.koview.BuildConfig
import com.example.koview.app.App.Companion.sharedPreferences
import com.example.koview.data.model.BaseState
import com.example.koview.data.model.requeset.ReIssueRequest
import com.example.koview.data.model.response.ReIssueResponse
import com.example.koview.data.model.runRemote
import com.example.koview.data.remote.AuthApi
import com.example.koview.presentation.utils.Constants.ACCESS_TOKEN
import com.example.koview.presentation.utils.Constants.REFRESH_TOKEN
import com.example.koview.presentation.utils.Constants.TAG
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject

class BearerInterceptor @Inject constructor() : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)

var newAccessToken: String? = null

// API 통신중 특정코드 에러 발생 (accessToken 만료)
if (response.code == 410) {

runBlocking {

val accessToken = sharedPreferences.getString(ACCESS_TOKEN, null)
val refreshToken = sharedPreferences.getString(REFRESH_TOKEN, null)

if (!accessToken.isNullOrEmpty() && !refreshToken.isNullOrEmpty()) {

val request = ReIssueRequest(accessToken, refreshToken)

getNewAccessToken(request).let {
when (it) {
is BaseState.Success -> {
// token 저장
sharedPreferences.edit()
.putString(ACCESS_TOKEN, it.body.result.accessToken)
.putString(REFRESH_TOKEN, it.body.result.refreshToken)
.apply()

newAccessToken = it.body.result.accessToken
}

is BaseState.Error -> {
// token 삭제
sharedPreferences.edit()
.remove(ACCESS_TOKEN)
.remove(REFRESH_TOKEN)
.apply()
Log.d(TAG, it.msg)
}
}
}
}
}

newAccessToken?.let {
val newRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $it")
.build()
return chain.proceed(newRequest)
}
}

return response
}

private suspend fun getNewAccessToken(tokenList: ReIssueRequest): BaseState<ReIssueResponse> {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build()

val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_DEV_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
val api = retrofit.create(AuthApi::class.java)
return runRemote {
api.refreshToken(tokenList)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.koview.data.model.requeset

data class ReIssueRequest (
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.koview.data.model.response

data class ReIssueResponse(
val isSuccess: Boolean,
val code: String,
val message: String,
val result: ReIssueResult,
)

data class ReIssueResult(
val grantType: String,
val accessToken: String,
val refreshToken: String,
)
16 changes: 16 additions & 0 deletions app/src/main/java/com/example/koview/data/remote/AuthApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.koview.data.remote

import com.example.koview.data.model.requeset.ReIssueRequest
import com.example.koview.data.model.response.ReIssueResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface AuthApi {

@POST("member/reissue")
suspend fun refreshToken(
@Body params: ReIssueRequest
): Response<ReIssueResponse>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.koview.data.repository

import com.example.koview.data.model.BaseState
import com.example.koview.data.model.requeset.ReIssueRequest
import com.example.koview.data.model.response.ReIssueResponse

interface AuthRepository {

suspend fun refreshToken(
body: ReIssueRequest
): BaseState<ReIssueResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.koview.data.repository

import com.example.koview.data.model.BaseState
import com.example.koview.data.model.requeset.ReIssueRequest
import com.example.koview.data.model.response.ReIssueResponse
import com.example.koview.data.model.runRemote
import com.example.koview.data.remote.AuthApi
import javax.inject.Inject

class AuthRepositoryImpl @Inject constructor(private val api: AuthApi) : AuthRepository {

override suspend fun refreshToken(body: ReIssueRequest): BaseState<ReIssueResponse> =
runRemote { api.refreshToken(body) }

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.example.koview.data.model.BaseState
import com.example.koview.data.model.requeset.SignInRequest
import com.example.koview.data.repository.IntroRepository
import com.example.koview.presentation.utils.Constants.ACCESS_TOKEN
import com.example.koview.presentation.utils.Constants.REFRESH_TOKEN
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -40,9 +41,6 @@ class LoginViewModel @Inject constructor(


fun checkLogin() {

// todo: 로그인 response accessToken 저장

val emailValue = email.value
val pwValue = password.value

Expand All @@ -58,6 +56,9 @@ class LoginViewModel @Inject constructor(
is BaseState.Success -> {
sharedPreferences.edit()
.putString(ACCESS_TOKEN, it.body.result.accessToken)
.putString(REFRESH_TOKEN, it.body.result.refreshToken)
.apply()

_loginCheckVisible.value = false
navigateToMain()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import com.example.koview.databinding.ActivitySplashBinding
import com.example.koview.presentation.base.BaseActivity
import com.example.koview.presentation.ui.intro.IntroActivity
import com.example.koview.presentation.ui.main.MainActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding::inflate) {

private val viewModel: SplashViewModel by viewModels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package com.example.koview.presentation.ui.splash

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.koview.app.App.Companion.sharedPreferences
import com.example.koview.data.model.BaseState
import com.example.koview.data.model.requeset.ReIssueRequest
import com.example.koview.data.repository.AuthRepository
import com.example.koview.presentation.utils.Constants.ACCESS_TOKEN
import com.example.koview.presentation.utils.Constants.REFRESH_TOKEN
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
Expand All @@ -15,16 +21,54 @@ sealed class SplashEvent {
}

@HiltViewModel
class SplashViewModel @Inject constructor() : ViewModel() {
class SplashViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {

private val _event = MutableSharedFlow<SplashEvent>()
val event: SharedFlow<SplashEvent> = _event.asSharedFlow()

fun checkLogin() {
// todo: 로그인 정보 있으면 바로 MainActivity로 이동
viewModelScope.launch {
_event.emit(SplashEvent.NavigateToIntroActivity)
// _event.emit(SplashEvent.NavigateToMainActivity)
val accessToken = sharedPreferences.getString(ACCESS_TOKEN, null)
val refreshToken = sharedPreferences.getString(REFRESH_TOKEN, null)

if (!accessToken.isNullOrEmpty() && !refreshToken.isNullOrEmpty()) {
// token 정보 존재할 때 메인 화면으로 이동
val request = ReIssueRequest(accessToken, refreshToken)
refreshToken(request)
} else {
// token 정보 없을 때 로그인 화면으로 이동
_event.emit(SplashEvent.NavigateToIntroActivity)
}
}
}

private fun refreshToken(tokenList: ReIssueRequest) {
viewModelScope.launch {
authRepository.refreshToken(tokenList).let {
when (it) {
is BaseState.Success -> {
// token 저장
sharedPreferences.edit()
.putString(ACCESS_TOKEN, it.body.result.accessToken)
.putString(REFRESH_TOKEN, it.body.result.refreshToken)
.apply()

_event.emit(SplashEvent.NavigateToMainActivity)
}

is BaseState.Error -> {
// token 삭제
sharedPreferences.edit()
.remove(ACCESS_TOKEN)
.remove(REFRESH_TOKEN)
.apply()

_event.emit(SplashEvent.NavigateToIntroActivity)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ object Constants {

const val TAG = "debugging"
const val ACCESS_TOKEN = "ACCESS_TOKEN"

const val REFRESH_TOKEN = "REFRESH_TOKEN"
}
10 changes: 5 additions & 5 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Koview" parent="Theme.Material3.Light.NoActionBar">

<style name="Theme.Koview" parent="Theme.Material3.Light.NoActionBar">
<!--버튼 또는 체크박스 등의 기본 테마색 설정-->
<item name="colorPrimary">@color/kv_main3</item>



<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:windowIsTranslucent">true</item>
</style>

<style name="Theme.Koview" parent="Base.Theme.Koview" />
<style name="Base.Theme.Koview" parent="Theme.Koview" />

</resources>