[Android] 토큰 처리 정리

panax·2023년 12월 6일
0

Android

목록 보기
13/16
post-thumbnail

🍪토큰

앱에서 로그인을 만들면 JWT를 써서 사용자 인증을 하게된다.
JWT를 사용해서 기능을 만들기는 하는데 어떻게 동작하고 정의된 지는 잘 몰라서 이번에 정리하려 한다.

토큰으로 인증 처리를 할 때 주의할 점도 정리했다.

🍪JWT(Json Web Token)

JWT는 RFC 7519로 정의되었고 JSON 객체를 사용해 안전한 통신을 제공한다고 한다.

HMAC을 사용하고 RSAECDSA을 사용해 암호키를 생성한다.

🍪구조

JWT는 Header, Payload, Signature로 구성되어 있다.
실제로는 세 부분이 xxx.yyy.zzz 같은 형식으로 조합된다.
구조를 테스트할려면 여기에서 할 수 있다.

Header와 Payload는 누구나 볼 수 있어 암호화되지 않은 정보는 넣으면 안된다.

🧃Header

헤더는 타입과 인증 알고리즘으로 구조가 나뉜다.
alg는 서명 알고리즘으로 HMAC SHA256, RSA 같은 것들이 있다.
작성된 헤더는 Base64Url로 인코딩된다.

{
  "alg": "HS256",
  "typ": "JWT"
}

🧃Payload

페이로드는 사용자 상태나 추가 정보를 가지는 클레임(claim)을 가지는데 3종류가 있다.

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Registered claims
미리 정의된 클레임이라 필수는 아닌데 사용하는 걸 추천한다고 한다.
iss(issuer), exp(expiration time), sub(subject) 등이 있다.

Public claims
사용자가 마음대로 할 수 있지만, 다른 JWT와 충돌할 수 있어서 미리 정의된 것을 쓰거나 URI를 사용해야 한다.

Private claims
서로 교환하기로 정의한 데이터

🧃Signature

시그니쳐는 헤더와 페이로드를 Base64Url로 인코딩한 정보와 secret를 가지고 있다.
시그니쳐로 메시지가 변경됐는지, 누가 보냈는 지 검증할 수 있다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

🍪작동 방식

JWT를 Authorization 헤더에 다음 형식으로 넣으면 서버에서 헤더에 있는 JWT를 검사한다.

Authorization: Bearer <token>

JWT의 크기가 너무 커서 헤더가 8KB보다 큰 경우 일부 서버가 차단할 수도 있다.

구글 로그인처럼 인증 서버를 따로 두는 방법과 API 서버에서 같이 처리하는 방법이 있는데,
여기서는 API 서버에서 처리하는 방법 기준으로 작성하려 한다.

  1. 로그인을 진행해서 서버에서 Access 토큰과 Refresh 토큰을 받는다.
  2. 토큰과 같이 API를 요청한다.
  3. 토큰이 만료됐으면 서버에서 에러를 보내는데, 일반적으로 401에러다.
  4. Refresh 토큰을 서버에 보내 새로운 토큰을 요청한다.
  5. 새로운 토큰을 받고 저장한다.
  6. 새로운 토큰으로 다시 API를 요청한다.

🍪토큰 사용 방법

Retrofit을 사용해서 토큰을 사용할 수 있는데 2가지 방법이 있다.
Authorization에 'Bearer'이 없는 경우도 있는데, 서버 스펙에 따라 달라서 잘 살펴봐야한다.

실제 작동하는 코드가 아니고 예시 코드로 작성되어 있다.

🧃Interceptor 방법

Retrofit의 Application Interceptor를 사용하면 서버에 보내기 전에 토큰을 추가할 수 있다.

/** 예시 코드입니다. 그대로 사용하면 작동 안됩니다. **/
class TestInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token: String = runBlocking { 
            "토큰을 가져와야 합니다."
        } ?: throw Exception("토큰이 없습니다.")

        val request = chain.request()
            .newBuilder()
            .header("Authorization", token)
            .build()
        
        return chain.proceed(request)
    }
}

🧃API 직접 입력 방법

토큰을 많이 안 쓰면 사용할 만한 방법이다.

@GET(value = "/users/{uid}")
suspend fun readUser(
    @Header("Authorization") accessToken: String = AuthToken.accessToken
)

🍪토큰 재발급

토큰을 넣어서 요청했는데 만료됐다고 응답이 오면 재발급을 해야 한다.
재발급은 생각보다 복잡한데 Retrofit의 NetworkInterceptor를 사용하면 처리할 수 있다.

Retrofit의 Authenticator를 사용하면 재발급 처리를 할 수 있다.
위의 Interceptor에서 재발급 처리를 할 수도 있지만 코드가 길어지고 복잡해진다.

/** 예시 코드입니다. 그대로 사용하면 안됩니다. **/
class AuthAuthenticator : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        val currentToken = runBlocking { "토큰 가져와야 합니다" }

        return synchronized(this) {
            val updatedToken = runBlocking {
                "다른 API 호출에서 토큰이 변경되지 않았는지 검사해야 합니다."
            }
            
						// 토큰
            val token = if (currentToken != updatedToken) {
                updatedToken
            } else {
                // 토큰을 재요청합니다. 중복 요청하면 안됨
                val newResponse = runBlocking { retrofit2.Response.success("토큰") }

                if (newResponse.isSuccessful) {
                    runBlocking { "새로운 토큰을 저장합니다" }
                    "새로운 토큰"
                } else null
            }

						// 재요청
            if (token != null) {
                response.request.newBuilder()
                    .header("Authorization", "$token")
                    .build()
            } else null
        }
    }
}

Authenticator는 아래처럼 추가할 수 있다.

OkHttpClient.Builder().authenticator(authAuthenticator)

Authenticator는 401 에러가 발생해야 동작한다.
서버가 401이 아니라 다른 걸 준다면 Interceptor를 따로 만들거나 서버에서 401을 보내도록 해야 한다.

🍪재발급 주의 사항

토큰 자체를 사용하는 건 어렵지 않지만, 재발급할 때는 주의가 필요하다.

🧃비동기, 동기 처리

Retrofitr은 비동기로 API를 요청하기 때문에 재발급 요청이 동시에 되는 문제가 발생할 수 있다.
위 코드에서도 synchronized와 runBlocking를 사용해 이런 문제를 처리했다.

아니면 OkHttpClient의 maxRequest를 1로 만들면 API 요청이 한번에 1개만 된다.

🧃토큰 재발급 방식

이거는 서버에 따라 달라진다.

Refresh 토큰을 API와 같이 요청하면 서버가 요청 결과 헤더에 새로운 토큰을 줄 수 있다고 한다.

아니면 401 에러가 발생하면 클라이언트가 직접 재발급 API를 호출해서 처리하는 방식도 있다.

개인적으로 헤더에 보내주는게 API 요청도 적고 처리가 깔끔해서 좋은 거 같은데,
서버에서 못하거나 안해주면 안되기 때문에 서버 개발자와 잘 협상해야 할 거 같다.

🧃토큰 저장 방식

토큰을 발급하고 저장하는 방식도 생각해야 한다.

DataStore 같은 걸 사용해 앱에 토큰을 저장하면 앱을 다시 실행해도 로그인할 필요가 없을 수 있다.
다만 토큰을 저장하기 때문에 따로 암호화를 하는 등 보안에 신경써야 한다.

굳이 저장할 필요가 없으면 전역 변수처럼 메모리에 저장할 수도 있다.
이런 경우 파일을 안 읽어도 되니 입출력 처리가 좀 더 편하고 메모리에만 있기 때문에 상대적으로 보안에 신경을 덜 써도 되지만, 앱을 다시 실행하면 로그인도 다시해야 한다.

🍪참고

JWT 정리
예시 코드

profile
안드로이드 개발자

0개의 댓글