[Android] Get JWT from HTTP response Header

Minji Jeong·2022년 5월 11일
1

Android

목록 보기
13/39
post-thumbnail
요즘 Retrofit을 사용해서 회원 로그인을 구현하고 있다. 전에 Retrofit에 대해서 간단하게 포스팅을 썼을 때는 단순히 POST method를 통해 필요한 데이터를 서버에 전달해 로그인을 구현하는 방법에 대해 설명했었는데, 이렇게 단순하게만 구현하면 애플리케이션 전체에서 로그인 상태를 유지할 수 없기 때문에 매번 새로운 화면에 진입할 때마다 로그인을 해야한다. 이건 매우 귀찮은 방법이고, 이 문제의 해결하기 위해 프로젝트에서 JWT라는 것을 사용하고 있다. 하지만 아직 HTTP의 요청 및 응답 구조에 대해 제대로 모르기 때문에 토큰을 응답 구조의 어느 부분에서 얻어야 하는지 감이 안섰고, 때문에 body, header 등 http 메세지의 구조 대해 공부하고 넘어가지 않으면 또다시 헤맬 것 같아서 이렇게 글을 남기게 되었다..! 먼저 문제를 해결하기 위해 사용자 인증 정보 관리 방법인 쿠키/세션/토큰과 HTTP Message에 대한 개념부터 짚고 넘어가자.
🚨 이 글은 안드로이드 환경에서 어떻게 HTTP 응답 메세지로부터 토큰을 가져올 수 있을까가 주제고, 이 문제를 해결하기 위해 사용자 인증 정보 관리 방법과 HTTP Message에 대해 기초적인 개념 위주로 작성했기 때문에 각 기능에 대해 완전 파고들듯이 기술하진 않았다. 따라서 완전 상세하게 나타낸 글을 보고 싶다면 다른 블로그를 보는 것을 추천한다!

HTTP

HTTP는 인터넷 상에서 데이터를 주고 받기 위한 서버/클라이언트 모델을 따르는 프로토콜이다. 클라이언트가 서버에게 요청을 보내면 서버는 응답을 보냄으로써 데이터를 교환하는데, HTTP는 비연결성 및 무상태성이기 때문에 요청에 대한 응답만 처리한다. 즉, 서버는 응답 이후 클라이언트에 대한 정보를 보관하지 않고, 따라서 동일한 클라이언트가 여러번 요청하더라도 클라이언트를 식별할 수 없다. 모바일로 따지면 동일한 사용자가 요청을 여러번 하더라도 매번 새로운 화면에 진입할 때마다 로그인을 해아하는 단점이 발생하는 것이다. 이를 해결하기 위해 Cookie, Session, JWT가 도입되었다.

쿠키는 정보를 유지할 수 없는, Stateless한 성격을 가진 HTTP의 단점을 해결하기 위해 도입되었다. 쿠키는 웹 서버가 브라우저에게 지시하여 사용자의 로컬 컴퓨터 내 파일 또는 메모리에 저장하는 작은 기록 정보 파일로, 일정 시간동안 데이터를 저장할 수 있어서 로그인 상태를 유지하거나 사용자 정보를 일정 시간동안 유지해야 하는 경우에 주로 사용한다. 쿠키에 담긴 정보는 인터넷 사용자가 같은 웹사이트를 방문할 때마다 읽히고 수시로 새로운 정보로 바뀔 수 있다. 쿠키는 key-value 형식의 문자열로 저장한다.

서버는 클라이언트의 로그인 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cookie에 담아 전달한다. 이후 해당 클라이언트는 요청을 보낼 때마다, 매번 저장된 쿠키를 요청 헤더의 Cookie에 담아 보낸다. 서버는 쿠키에 담긴 정보를 바탕으로 해당 요청의 클라이언트가 누군지 식별할 수 있게 된다.

하지만 쿠키는 보안에 취약하고, 용량 제한이 있어 많은 정보를 담을 수 없으며, 웹 브라우저마다 쿠키에 대한 지원 형태가 다르기 때문에 브라우저간 공유가 불가능하다는 단점이 있다.

Session

세션은 쿠키의 보안적인 이슈를 해결하기 위해 등장했다. 비밀번호 등 클라이언트의 인증 정보를 쿠키가 아닌 서버 측에 저장하고 관리하는 것이다. 하지만 세션 역시 단점이 있다. 요청을 할 때마다 세션 저장소에 세션 ID를 조회하는 작업을 통해서 DB 접근이라는 로직이 한번 더 수행된다는 것이다.

JWT (Json Web Token)

JWT는 사용자 인증에 필요한 정보들을 암호화시킨 토큰으로, JWT를 HTTP 헤더에 실어 서버가 클라이언트를 식별할 수 있도록 하는 게 JWT 기반 인증 방식이다. 기존의 쿠키 기반 인증은 브라우저간 공유가 불가능하고, 세션기반 인증은 서버가 파일이나 데이터베이스에 저장된 세션 정보를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생한다는 단점이 있지만, JWT 인증 방식은 클라이언트에 토큰을 부여하면 끝이다. JWT는 로컬스토리지/세션/쿠키에 안전하게 저장된 상태로 클라이언트에 전달되어야 하는데, 어디에 저장할지는 각자 장단점이 있기 때문에 잘 고려해서 선택하면 된다. 현재 내가 진행중인 프로젝트에선 쿠키에 토큰을 저장하는 방식을 사용하고 있다.

JWT는 다른 로그인 시스템에 접근 및 권한 공유가 가능하고, 서버가 파일이나 데이터베이스에 저장된 세션 정보를 조회할 필요가 없다는 장점이 있다. 하지만 쿠키/세션과 다르게 길이가 길어서 인증 요청이 많아질수록 네트워크 부하가 심해질 수 있고, 토큰이 한번 발급되면 유효기간이 만료될 때까지 계속 사용이 가능하기 때문에 탈취당하면 대처하기 어렵다는 단점이 있다. 이 문제는 기존의 Access Token보다 유효기간이 긴 Refresh Token을 사용하여, 매번 새로운 액세스 토큰을 갱신하는 것으로 해결할 수 있다.

Access Token & Refresh Token

기존에 사용했던 Access Token을 통한 인증 방식은 보안에 취약해 유효기간이 짧은데, 유효기간이 짧으면 매번 토큰을 발급받아야 하므로 불편하다. 하지만 Refresh Token은 Access Token과 똑같은 형태의 JWT로 긴 유효기간을 가지면서, Access Token이 만료되었을 때 새로 발급해준다. Refresh Token은 검증을 위해 별도의 로컬 스토리지에 저장해야한다.


종합해보자면 이렇다. 안드로이드에서 모바일 기능을 구현할 때, 클라이언트가 사용자의 아이디와 비밀번호, 자동 로그인 여부를 파라미터로 전달하여 서버에 로그인을 요청하면, 서버는 전달받은 데이터를 통해 유효한 사용자인지 검사한 후 유효한 사용자라면 access token과 자동 로그인을 위한 refresh token을 쿠키에 감싸서 클라이언트에게 응답하는 것이다. 이후 전달받은 refresh token을 DataStore와 같은 클라이언트의 로컬 스토리지에 저장하여 자동 로그인에 사용할 수 있다. 이렇게 클라이언트와 서버간 커뮤니케이션을 위해 우리는 '요청''응답'으로 이루어진 HTTP Message를 사용해야 한다. 일단 HTTP Message의 구조를 살펴보자.


HTTP Message


1. Start line
요청이나 요청에 대한 응답 상태를 나타낸다. 응답에서는 status line이라 부른다.
2. Headers
요청을 지정하거나 메세지에 포함된 본문을 설명하는 헤더 집합이다.
3. Empty line
헤더와 바디를 구분하는 빈 줄이다.
4. Body
요청/응답과 관련된 데이터를 포함하는데, 요청과 응답의 유형에 따라 선택적으로 사용한다.

HTTP Message - Request


Method
GET, POST, PUT, DELETE 등 클라이언트가 수행하고자 하는 동작이다. 클라이언트는 GET을 사용해서 데이터를 조회하거나 POST를 사용해서 데이터를 서버로 전송할 수 있다.
Version of the protocol
HTTP 프로토콜의 버전이다.
Headers
서버에 대한 추가 정보를 전달하는 헤더이다.

HTTP Message - Response


Status Code
요청의 성공 여부와 그 이유를 나타내는 상태 코드이다. 요청에 성공하면 200, 클라이언트가 잘못된 요청을 했을 경우엔 400, 서버 내부에 오류가 있다면 500을 볼 수 있다.
Status Message
Status Code를 짧은 설명으로 나타내는 상태 메세지다.
Headers
요청 헤더와 비슷하다.

HTTP 요청 메세지와 응답 메세지가 어떤 구조인지 간단하게 살펴봤다. 어쨌든 나는 이 응답 메세지에서 token을 전달받아야 한다. 그런데 이 구조에서 어떻게 access token과 refresh token을 얻으라는 것일까?

HTTP Message에 대해 공부하기 전, 나는 요청과 응답 메세지의 구조가 어떻게 생겼는지 제대로 몰랐다. 따라서 모든 데이터는 Body에 담겨져서 온다고 생각했고, 그렇기에 토큰도 당연히 Body에 있을거라고 생각했는데 아무리 찾아봐도 없는 것이다 😥 메세지 뿐만 아니라 쿠키와 토큰이 뭔지도 제대로 몰랐으니 헤맨게 당연했다. 여튼 프로젝트에서 사용중인 로그인 API를 테스트를 하면서 token이 어디에 있는지 알 수 있었다. 나는 테스트를 위해 API 테스트 플랫폼인 POSTMAN을 사용해보기로 했다.

POSTMAN의 기본적인 화면은 이렇다. GET/POST/PUT 등 다양한 요청 method를 테스트 해 볼 수 있는데, 나는 로그인을 테스트해야 했으니 POST method를 사용했고, Body로 아이디와 패스워드를 보냈다(위 화면의 모든 데이터들은 내가 임의로 집어넣은 것이다!).

응답 결과는 Body, Cookies, Headers로 나눠서 볼 수 있다. 역시 Body에는 내가 원하는 토큰값이 존재하지 않았다. 토큰값은 Cookies와 Headers->Set-Cookie 메뉴에 존재했다.

Cookies
---------------------------------------
Name			Value			Domain
---------------------------------------
accessToken  	 ...
refreshToken  	 ...
Headers
----------------------------------------
Key				Value
----------------------------------------
Server			 ...
Date			 ...
Content-Type	 ...
Content-Length	 ...
...
Set-Cookie	  accessToken=...
Set-Cookie	  refreshToken=...
...

처음엔 body 내의 데이터를 갖고오는 것처럼 Retrofit 내에서 쿠키 값을 바로 가져오는 기능은 없을까 찾아봤는데, 분명 더 좋은 방법이 있을 것 같지만😥 시간이 부족해서 일단 StackOverFlow에서 방법을 찾았다. StackOverFlow에서 설명하는 건 Headers에서 Set-Cookie 부분을 직접 파싱해서 각각의 쿠키 내에 저장된 토큰값들을 가져오는 것이다.
StackOverFlow - How to retrieve cookie from response retrofit, okhttp?

🔵 2022.05.17 추가

응답 헤더로부터 토큰값을 가져와 직접 로컬 스토리지에 저장할 필요 없이, CookieJar라는 걸 사용하면 서버로부터 응답받은 쿠키를 다음 요청 시 자동으로 사용할 수 있다!! 관련 포스팅을 아래 링크로 달아놨으니, 참고해서 간단하게 코드 몇 줄만 추가하자.
[관련 포스팅] retrofit + CookieJar를 사용한 쿠키 유지
서버가 생성된 쿠키를 저장하는 헤더로, 응답시 클라이언트에게 전달된다.

<소스코드 💻>

NetworkClient.loginService.login(LoginData(user_id, user_pw, auto_login))
.enqueue(object: Callback<LoginResponse> {
		override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>){
        	if (response.isSuccessful.not()){
            	Log.e(TAG, response.toString())
                return
            }else{
            	val header = response.headers()
                if (autoLogin){ //자동 로그인에 체크했다면
                	//응답 헤더의 Set-Cookie 파싱해서 refresh token값 가져오기
                	val rt = header["Set-Cookie"]?.split(";")?.get(0)
                    val refreshToken = rt?.replace("refreshToken=","")
                    //DataStore에 저장
                }
            }
		}
         override fun onFailure(call: Call<LoginResponse>, t: Throwable){
         		Log.e(TAG, t.toString())
     	}
})

서버로부터 받은 refresh token을 로컬 스토리지에 저장하고, 이후 앱을 실행할 때마다 refresh token을 요청 헤더에 담아 보내면 자동 로그인 기능을 구현할 수 있다.

후기 😊
이번 포스팅을 작성하면서 HTTP 요청/응답 메세지 뿐만 아니라 쿠키,세션,토큰의 개념에 대해서도 공부할 수 있었다. 사실 Retrofit을 사용하면서 HTTP methods만 제대로 쓸 줄 알면 큰 문제는 없지 않을까 생각했는데, 소셜 로그인이 아닌 우리가 만드는 서비스의 회원 로그인을 구현하다보니 보안 면에서도 신경 쓸 것이 많고, 내가 클라이언트-서버 간 요청과 응답에 대해 제대로 공부하지 않아서 많이 헤맬 수 밖에 없었다는 생각이 든다. 안드로이드 개발이 프론트에 가깝다지만 프론트와 백엔드간의 커뮤니케이션은 필수고, 이에 따라 네트워크 관련 작업을 구현하면서 기본적인 것들은 꼭 공부해야겠다고 다짐했다.

References

https://tecoble.techcourse.co.kr/post/2021-05-22-cookie-session-jwt/
https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-JWTjson-web-token-%EB%9E%80-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC
https://habit1014.tistory.com/85
https://velog.io/@gparkkii/HTTPMessage

profile
Flutter Developer

0개의 댓글