[Android, DRF] API 토큰 기반 로그인 구현하기

 

코틀린 코드로 토큰 기반 로그인 구현 

 

자주 사용하는 기능이기에 정리한다.

토큰 기반 인증은 모던 웹서비스에서 많이 사용하는 방법이며,

API 기능에서 사용하는 인증 작업중에 가장 좋은 방법인 것 같다.

 

[토큰 인증을 사용하는 이유]

 

-  유저의 인증 정보를 서버나 세션에 담아두지 않아도 됨 (Stateless)

-  서버는 클라이언트 측에서 오는 요청만 처리하므로 서버와 연결고리가 적음 (Scalability)

-  서버에 요청을 보낼 때 쿠키를 전달하지 않기 때문에 보안이 높음 

 

 

[토큰 인증의 작동 원리]

 

1. 유저가 아이디 + 비밀번호 등으로 로그인

2. 서버측에서 해당 계정정보를 확인

3. 정확한 정보일 경우 유저에게 토큰 발급

4. 클라이언트가 서버에서 받은 토큰을 저장

5. 서버에 새로운 요청을 보낼 때마다 해당 토큰을 함께 서버에 전달

6. 서버에서 토큰을 검증하고 유효하다면 요청에 응답

 

 

[동작 원리에 따른 토큰 인증 방식 적용]

 

- 안드로이드에서 서버와 연동하기 위해서 작동 원리 순으로 코드를 작성한다.

 

 

1. 아이디 + 비밀번호 로그인 기능 구현 (2번, 3번, 4번 기능 까지 구현)

 

- 로그인을 요청하는 기능을 레트로핏에 등록한다.

- 유저의 아이디와 비밀번호를 전송하며 응답으로 token이 들어있는 데이터를 받는다.

 

< 로그인 요청 >

//로그인 요청
@POST("user/login/")
@FormUrlEncoded
fun loginUser(
    @Field("userName") userName: String,
    @Field("password") password: String
): Call<User>

 

<응답 데이터>

class User(
    var userName : String ,
    var last_login : String?,
    var token : String?
):Serializable

 

2번,3번,4번. 서버에서 계정 정보를 확인하고 유효하다면 토큰 전송 -> 클라이언트에서 토큰 저장

 

- 로그인 액티비티에서 로그인 요청을 보낸다.

- 로그인 함수 작성

- loginUser -> 서버에 아이디 + 비밀번호 POST

- saveUserToken() 함수를 통해서 안드로이드 내부에 전달 받은 토큰을 저장한다.

private fun login(){

    (application as MasterApplication).service.loginUser(
        getUserName(),getUserPass()
    ).enqueue(object :Callback<User>{
        override fun onResponse(call: Call<User>, response: Response<User>) {
            (application as MasterApplication).createRetrofit()
            if (response.isSuccessful && response.body()!=null){

                //로그인 성공
                val user = response.body()
                //토큰 가져오기
                val token = user!!.token
                if (token!=null){
                    saveUserToken(token,this@LoginActivity)
                }
                //다음 화면으로 넘어가기
                val intent = Intent(applicationContext,MainActivity::class.java)
                intent.putExtra("username",getUserName())
                startActivity(intent)
            }else{
                val err = errorConvert(response.errorBody())
                Toast.makeText(this@LoginActivity,err,Toast.LENGTH_SHORT)
                    .show()
            }
        }

        override fun onFailure(call: Call<User>, t: Throwable) {
            Toast.makeText(this@LoginActivity,"Network error",Toast.LENGTH_SHORT)
                .show()
        }

    })

}

 

- saveUserToken()

- 안드로이드 내부에 토큰 저장 

- 비밀번호를 저장하는 것보다 보안에 안전함

private fun saveUserToken(token:String,activity: Activity){
    val sp = activity.getSharedPreferences("login_sp", Context.MODE_PRIVATE)
    val editor = sp.edit()
    editor.putString("login_sp",token)
    editor.apply()
}

 

5. 서버에 새로운 요청을 보낼 때마다 해당 토큰을 함께 서버에 전달

 

- 이제 저장한 토큰을 활용해야 한다

- 안드로이드에서 서버에 전송할 때 MasterApplication을 선언해서 서버와 통신한다.

- MasterApplication은 서버와 통신할 때 헤더에 토큰을 담아서 전송하거나 레트로핏 통신을 생성한다.

 

<MasterApplication의 createRetrofit() 함수>

  - 헤더를 생성해서 이전에 저장한 토큰을 담는다.

  - 2가지 함수를 사용한다.

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

- checkIsLogin() : 저장 된 토큰이 있는지 확인하는 함수

- getUserToken() : 저장 된 토큰을 가져오는 함수 

- createRetrofit()에서는 헤더를 생성하고 토큰이 있다면 값을 넣어준다.

- 이후에 레트로핏을 생성할때 헤더 정보를 담은 클라이언트를 레트로핏에 넣어준다.

- 이제 클라이언트에서 서버와 통신할 때 토큰 정보를 같이 전송한다.

fun createRetrofit(){

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

    //오류 관련 인터셉터
    val logInterceptor  = HttpLoggingInterceptor()
    logInterceptor.level = HttpLoggingInterceptor.Level.BODY

    val client = OkHttpClient.Builder()
        .addInterceptor(header)
        .addInterceptor(logInterceptor)
        .addNetworkInterceptor(StethoInterceptor())
        .build()

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

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

 

6. 서버에서 토큰을 검증하고 유효하다면 요청에 응답

 

- 이제 서버측에서 해당 토큰이 유효하다면 클라이언트에 값을 전해준다.

- 토큰이 존재할 때 header에 아래 방식으로 토큰을 넣어준다.

- 서버측에서는 Authorization : token "토큰" 과 같은 형식으로 데이터를 받는다.

 

val request =original.newBuilder()
     .header("Authorization", "token $token")
     .build()

 

Authorization: <type> <credentials>

위와 같은 형식으로 데이터가 전달 되는데 type은 서버측에서 설정한 것을 따르며,

DRF에서는 token "token" 방식으로 인증한다.

다른 프레임워크에서는 bearer "token" 방식으로 인증하는 것 같다.