이것만은 알고가자! 토큰토큰

Breeze·2021년 11월 17일
1

Backend 개발 일지

목록 보기
1/7

안녕하세요 행복이님들~ 채니챈이에요🙂 제대로 된 첫 연재글입니다 후후~ 오늘은 웹 사이트 구현시 거의 필수적으로 접하게 되는 JWT에 대해서 정리해보았어요! 우리 프로젝트에는 어떻게 적용할지 계획도 적어보았으니 구독과 좋아요 꾹 눌러주세요💜💜

인증과 인가

우선 우리가 1학기 때 잠시 배우고 지나갔던 인증과 인가부터 살펴보고 가실게요~!!

인증은 사용자가 자기 계정을 사용하려고 할 때 로그인해주는 것이었고

인가는 서버가 인증을 받은 사용자임을 알아보고 허가해주는 것이라고 기억하고 있겠죠??

그런데 이 인가 과정에서 사용자의 로그인 여부를 아이디랑 비번을 매 요청마다 보내게 되면 데이터베이스에 저장된 사용자 계정의 해시값을 꺼내온 뒤 사용자의 암호를 복잡한 알고리즘으로 계산한 값과 일치하는지 확인하는 과정 등 굉장히 복잡해집니다!

그래서 이를 대체하고자 나온 개념이 세션과 토큰입니다!! 그럼 이제 세션과 토큰을 알아볼까요💜

세션

사이트와 특정 브라우저 사이의 state(상태)를 유지시키는 것

Session ID를 사용해서 사용자가 서버에 로그인 되어있다는 것이 지속되는 상태

1. 사용자가 로그인하면 서버는 Session ID를 쿠키에 담아 전송
2. 사용자는 브라우저에 쿠키를 저장
3. 브라우저는 앞으로 요청을 보낼 때마다 Session ID가 담긴 쿠키(브라우저에 저장되는 정보)를 실어서 보냄
4. 서버는 브라우저의 Session ID를 데이터베이스와 비교해서 있으면 인가!

하지만 이 작업은 문제가 있습니다. 메모리에 Session ID를 넣어두면 사용자가 동시에 많이 접속을 하면 메모리가 부족해질 것이고 게다가 휘발성이므로 서버가 재부팅 시 로그인이 튕기겠죠. 데이터베이스 서버에 넣어두고자 레디스와 같은 메모리형 데이터베이스 서버를 사용하는 방법도 있긴 하다고 합니다. 하지만 보안이 취약한 쿠키에 Session ID를 담아야 하는 문제는 여전합니다.

이러한 번거로움을 해결하기 위해 나온 것이 바로 JWT 토큰!!

JWT

JSON Web Token

Json 형식으로 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token

사용자가 로그인하면 토큰을 출력해주는데 서버가 무언가를 기억하고 있지 않을 수 있습니당!

  • header: 토큰의 타입과 서명 값을 만드는데 사용하는 알고리즘
  • payload (Base64): 토큰에 담긴 사용자 정보 등의 데이터 (누가누구에게 발급했는지, 토큰 유효기간은 얼마인지, 서비스가 사용자에게 이 토큰을 통해 공개하기 원하는 내용) = Claim
  • verify signature: 헤더와 페이로드, 서버의 비밀키를 암호화 알고리즘에 넣고 돌리면 나오는 것
1. 클라이언트가 유저 로그인 요청
2. 서버에서 클라이언트에게 JWT를 발급
3. 클라이언트에서는 이후 요청시 발급받은 JWT를 함께 전송
4. 서버에서 JWT에 포함된 Signature를 확인 후 클라이언트의 요청에 응답

장점으로는

  • Statelessness & Scalability(무상태성 & 확장성): 서버에서 클라이언트 정보를 저장할 필요가 없고 토큰을 헤더에 추가만 하면 인증 절차가 완료 되겠죠!
  • 안정성: 암호화 한 토큰을 사용하므로 암호화 키를 노출할 필요가 없겠네요!

하지만 토큰이 정보를 담고 있는데 이 토큰의 상태를 관리하기 어렵다는 단점이 있겠네요🙄

이를 해결하기 위한 방법으로 나온 것이 Refresh Token과 Access Token 입니다.

1. 클라이언트가 유저 로그인 요청
2. 서버에서 유저 DB에서 유저 정보 확인하고 클라이언트에게 Access Token, Refresh Token 발급
3. 서버의 DB에 Refresh Token 저장 (일반적으로 유저 DB에 저장)
4. 클라이언트는 Refresh Token을 Local Storage에 저장하고 Access Token을 헤더에 실어 요청
5. 서버는 Access token을 검증하여 이에 맞는 응답

Access Token 만료된 경우
1. 클라이언트에서 Access Token의 PayLoad를 통해 유효기간을 확인하고 만료되었다면 
   Refresh Token과 Access Token을 함께 보내며 재발급 요청
2. 서버는 클라이언트가 보낸 Refresh Token과 유저 DB에 저장된 Refresh Token을 비교 확인하고 
   유효기간이 지나지 않았다면 새로운 Access Token 재발급

Access Token의 경우 수명을 매우 짧게 주고 Refresh Token의 경우 꽤 길게 보통 2주 정도로 준다고 합니다. Refresh Token만 안전하게 관리된다면 이게 유효할 동안은 Access Token이 만료될 때마다 다시 로그인 할 필요 없이 새로 발급 받을 수 있겠죠! 중간에 Access Token이 탈취당해도 오래 쓰지는 못할 것입니다. 어떠한 사용자를 강제 로그아웃 시키려면은 리프레시 토큰을 갖다가 DB에서 지워서 토큰 갱신이 안되게 하는 방법을 활용할 수도 있겠네요!


그래서 우리는?

우리도 이러한 방법을 사용할 예정이랍니다🧚‍♀️🧚‍♀️ 자, 그러면 이쯤에서 우리의 킹갓제너럴 능력자 손준희 코치님께서 주신 답변을 토대로 다시 정리해볼게요!

JWT 토큰으로 Access Token과 Refresh Token 둘다 발급하게 되는데

  • Access Token이 만료되지 않은 경우
    • Access Token으로 백엔드 ↔ 프론트엔드 인증 과정을 거칩니다.
  • Access Token이 만료된 경우
    • 서버에서는 만료됐다는 응답을 보내고
    • 클라이언트에서는 Refresh Token을 함께 다시 보내서 새로운 Access Token(과 Refresh Token)을 요청하여 받게 됩니다

즉, Access Token은 평상시의 인증과정을 하게 되고 Refresh Token은 액세스 토큰이 만료됐을 때 요청을 해서 새로 Access Token을 받아오는데 사용합니다. 그러므로 Access Token은 유효기간을 짧게, Refresh Token은 매우 길게 설정하면 되겠죠!

그러면 Refresh Token은 서버의 어디에 저장하면 좋을까요?

코치님은 Refresh Token은 DB에 저장해두고 있다가 Access Token 재발급 요청 때 받은 Refresh Token과 DB의 값을 비교하여 사용하셨다고 합니다

1. 사용자 로그인
2. 서버에서 DB 확인
3. JWT(Access, Refresh) 발급
4. 클라이언트에 보냄
5. 클라이언트에서 각각 안전한 장소(?)에 보관
6. 일반적 통신은 Access로 인증함
7. 유효 기간 내에는 Access가 사용 가능하지만, 
   비교적 짧은 유효시간을 갖는 Access가 만료되면 서버에서 만료됨을 클라이언트에 알림
8. 클라이언트에서는 Refresh를 갖고 Access를 갱신해달라고 요청
9. 서버에서는 갱신된(새로 발급된) Access를 클라이언트에 보냄

이런 과정을 거치는데 사실 Refresh Token도 사용자 로그인으로 인해 발급된 JWT이기 때문에 자체만으로 인증 정보를 갖고 있다고 합니다. 그래서 이를 통해 그냥 Access Token을 새로 발급받아도 된다고 하네요!! 즉, DB에 Refresh Token을 저장할 필요가 없다는 것인데요!

그러면 유효기간이 긴 Refresh Token이 유효기간이 짧은 Access Token에 비하면 탈취 당했을 때 위험이 크지 않을까요? 이에 대해서는 Refresh Token은 인증 과정을 위해 사용하지 않고 갱신만을 위해서 사용하기 때문에 절대적인 빈도가 더 적으므로 탈취당하는 위험에 대해서는 덜 고려한다고 합니다.

기업에는 그마저도 고려해야 하니까 추가적인 검증과 로그인 유저 밴(강제 로그아웃) 과정을 위해 Refresh Token을 DB(이런 경우에는 레디스)에 저장할 수도 있다고 알려주셨습니다. 서버 자체적으로 추가적인 검증을 하거나, 현재 로그인한 유저를 내쫓는 법을 사용하려면 DB에 저장하는 방법을 사용한다는 것이지요!


자, 그러면 이제 우리는 어떻게 구현을 할 것이냐!

저희는 우선 Refresh Token을 DB에 저장하기로 하였습니다. Refresh Token 자체만으로도 유저 정보를 얻을 수 있다고 하셨기 때문에 이것으로 DB에서 유저를 찾아 해당하는 Refresh Token을 비교하고 Access Token을 발급할 수 있겠지만 이 과정에서 axios 요청이 두 번 가게 된다는 게 마음에 걸렸습니다😭 Access Token을 날렸을 때 만료로 인해 오류가 난다면 클라이언트에서 Refresh Token을 들고와서 다시 요청을 보내야하는 것이 다소 번거롭다고 여겨진 것입니다. (물론 저희의 개인적인 생각입니다.. 호호)

그래서 로그인이 필요한 모든 axios 요청에 user_id를 함께 넘겨주는 방법을 생각해보았습니다. 즉, 요청시 Access Token과 id를 함께 넘겨주게 되는 것입니다. 다음의 과정을 데코레이터로 만들어 붙이는 방법으로 구현해보았습니다. 이 부분은 우리의 황금막내를 넘은 다이아몬드 막내 귀여운 수빙수가 맡아주었고 혹시 잘못된 로직이 있다면 수정 부탁드립니다💎

**로그인이 필요한 부분에 모든 axios 요청에 id를 넘겨주기**

클라이언트에서 Access Token, id와 함께 요청을 보낸다

- Access Token이 유효하다면: 정상적으로 응답
- Access Token이 유효하지 않다면:
    - 함께 보내온 id로 서버의 유저 DB에서Refresh Token를 찾는다
    - 이렇게 찾은 Refresh Token으로 Access Token을 새롭게 발급받는다
    - Refresh Token이 갱신되면 모델에 다시 저장해준다

약간의 변경 사항 추가 ✨

원래는 위의 방식처럼 구현하려고 했지만, 쿼리스트링으로 날리기에는 뭔가 보안이 취약할 것 같다는 우리들의 생각으로 인해... header에 포함시켜서 전달하는 방법으로 변경했습니다. 로그인 데코레이터의 전체적인 로직을 정리해보면 아래와 같습니다.

**클라이언트에서 로그인이 된 상태에서 보내는 모든 axios 요청 header에 X-Id라는 항목으로
사용자의 ID를 함께 넘겨주기
Access Token 또한, Authorization 항목으로 넘겨주기

[ Access Token 유효성 검사 ]**
- 유효하다면: 
	- request.access_token에 0을 할당한다.
- 유효하지 않다면:
	- 함께 보내온 id로 서버의 유저 DB에서 Refresh Token를 찾는다
  - 이렇게 찾은 Refresh Token으로 Access Token을 새롭게 발급받는다
  - Refresh Token이 갱신되면 모델에 다시 저장해준다.
	- 새로 저장된 user 객체를 request.user에 할당시켜준다.
	- request.access_token에 새로 발급 받은 access token을 할당시켜준다.

명예 행복이

이것으로 JWT에 대한 간단한 연재글을 마무리하겠습니다. 방식과 과정이 무궁무진할테지만 우선은 기본적인 흐름만 알아둬도 구현에 큰 문제는 없을 것입니다!!

상세한 설명 주신 손준희 코치님께 무한한 감사를 드리며 명예 행복이 타이틀을 드리겠습니다🎁 (거절은 거절입니다.) 인가 과정에 대해 보다 자세한 내용이 궁금하시다면 공식문서를 확인해주세요~!!

OAuth 2.0 - OAuth

profile
약속 관리 서비스 breeze의 개발 일지입니다.

0개의 댓글