JWT 토큰 기반 인증에 대하여

Gorae·2022년 2월 20일
7

(TIL) CS

목록 보기
5/6
post-thumbnail

jwt 토큰을 이용한 인증에 대한 이해를 다뤘습니다. 공부 중이라 잘못된 부분이 있을 수 있으니, 지적해 주시면 감사드리겠습니다 🙏

Spring boot와 React로 간단한(간단하지 않은) 쇼핑몰을 만드는 팀 프로젝트를 진행 중에 있는데, JWT 토큰을 이용한 로그인을 구현하기로 했다. 관련 학습 자료의 양은 방대했지만 이해하기까지 꽤 오랜 시간이 걸렸다. 잊어버리기 전에 정리하기 위해 글을 쓴다.

🌱 기본 용어부터 알고 가자, 인증과 인가의 차이

인증 (authentication) : 인증을 하고 로그인을 하는 것(가입된 유저임을 인증)
인가 (authorization) : 인증된 사용자에게 허가해 주는 활동(ex. 마이페이지 정보 조회)

❓ 토큰 기반 인증이 등장한 이유?

참고 - 쿠키와 세션의 차이
기존의 인증 방식은 서버(세션) 기반 인증이다. 서버 기반 인증 로직은 다음과 같다.

  1. [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
  2. [ 서버 ]는 세션을 생성, 저장한다
  3. [ 서버 ]는 세션 id 를 쿠키에 담아 [ 클라이언트 ]에게 전달한다
  4. [ 클라이언트 ]는 요청 시 세션 id 를 쿠키에 담아 [ 서버 ]에 전달한다
  5. [ 서버 ]는 전달받은 쿠키의 세션 정보와, DB에 저장된 세션 정보를 비교한다
  6. [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다

이는 서버에서 유저 정보를 저장, 관리하기 때문에 매 요청 시마다 서버와 통신하게 된다.
때문에 다음과 같은 문제가 생길 수 있다.

  • 문제 1. 유저가 아주 많아질 경우, 서버에 부담이 될 수 있다
  • 문제 2. 트래픽이 증가하여 서버 한 대로 감당이 어려울 경우, 서버를 여러 대로 확장시킬 수 있는데, 이때 유저 정보를 관리하기가 까다로워진다.

결국 토큰 기반 인증은, 서버에서 유저 정보를 관리하지 않기 위한 대안이라고 보면 된다.

💡 토큰 기반 인증이란?

서버 기반 인증 시스템의 단점을 극복한, 무상태확장성이 핵심이다. 유저 정보를 서버나 세션에 담아두지 않고, 토큰에 담아 전달하기 때문에, 서버에서는 유저 정보를 저장하거나 관리할 필요가 없고, 서버 컴퓨터가 여러 대일지라도 큰 어려움 없이 인증/인가를 해줄 수 있다.

그리고 토큰에는 유효 기간이 있다. 만료 시간은 정하기 나름인데, 만료 시간이 짧으면 보안이 강화되지만 서버와의 통신이 잦아진다는 부담이 있다. 만료 시 재발급 로직도 정하기 나름인데, 여기서는 서버에서 토큰 만료를 먼저 알아챈 후 에러 메세지를 보내면, 클라이언트가 재발급 요청을 수행하는 로직으로 만들었다.(axios interceptors 사용)

토큰 기반 인증 로직은 다음과 같다.

  1. [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
  2. [ 서버 ]는 유저 정보를 검증한 후, 정확하다면 [ 클라이언트 ]에게 토큰을 발급해 준다
  3. [ 클라이언트 ]는 전달받은 토큰을 안전한 곳에 저장한다
  4. [ 클라이언트 ]는 요청 시 저장된 토큰을 헤더에 담아 [ 서버 ]에 전달한다
  5. [ 서버 ]는 전달받은 토큰을 검증한다
  6. [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다
  7. [ 서버 ]는 토큰 만료 시, [ 클라이언트 ]에게 에러 메세지를 전달한다
  8. [ 클라이언트 ]는 [ 서버 ]에 토큰 재발급 요청을 보낸 후, 새로 발급받은 토큰으로 갱신 후 저장한다

대충 보면 이해되는 듯 하나, 자세히 파고들면 의문이 생긴다.

    1. 클라이언트는 안전한 곳에 토큰을 저장한다? 거기가 어딘데?
    1. 서버는 토큰을 검증한다? 서버에 저장된 유저 정보가 없는데 어떻게?

차례대로 하나씩 풀어보려 한다.

❓ 클라이언트는 토큰을 어디에 저장해야 하나?

참고 - 프론트에서 안전하게 로그인 처리하기
결론부터 말하면, 웹 스토리지와 쿠키에 저장할 수 있고, 쿠키에 저장하는 것이 보다 더 안전하다. 클라이언트단에 저장되는 정보는 보안에 취약하기 때문에, 서버에서 어느 정도 제어할 수 있는 쿠키에 저장하는 방식이 더 추천된다. 하지만 어디든 토큰이 탈취되지 않으리라는 보장은 없기 때문에, 암호화하여 저장하는 것이 좋다. 비교 요약은 다음과 같다.

1. 웹 스토리지(localStorage, sessionStorage)에 저장

  • 👍 브라우저 로컬 저장소를 이용하는 것으로, 구현하기 쉽다
  • 👍 하나의 도메인에 국한되지 않으며, 도메인 당 최대 5~10MB까지 저장 가능하다
  • 💩 스크립트 공격에 취약하다(XSS 공격)
    • 해결 : 해킹에 사용될 수 있는 코딩에 사용되는 입출력 값을 검증하여 무효화시킴

2. 쿠키에 저장

  • 👍 httpOnly 로 쿠키를 설정하면, XSS 해킹 문제를 해결할 수 있다
  • 👍 Secure 옵션을 주면 https 로만 쿠키가 전송되기 때문에, 보안을 강화할 수 있다
  • 💩 최대 4MB까지 저장 가능하다
  • 💩 한정된 도메인에서만 쿠키를 사용할 수 있다
    • 해결 : 토큰이 필요할 때 기존 토큰으로 새 토큰을 받아올 수 있도록 api 설계
  • 💩 CSRF 공격 위험이 있다.
    • 해결 : 허용한 url에서만 요청을 받도록 함

❓ 그럼 서버에서 토큰을 어떻게 검증해?

이에 대한 의문은 JWT 토큰이 생성되는 방식을 보면 풀릴 수 있다.

💡 JWT 토큰이란?

공식 사이트

JWT : JSON Web Token

JWT 토큰은 Header, Payload, Verify Signature 세 부분으로 나뉘며, xxxx.yyyy.zzzz 의 형식으로 생성된다.

  • Header :
    Header, Payload, Verify Signature 를 암호화할 방식(alg), 타입(Type) 등을 포함
  • Payload :
    서버에서 보낼 데이터를 포함(userId, 유효기간 등)
  • Verify Signature :
    Base64 방식으로 인코딩한 Header, Payload, Secret key 를 더한 후 서명됨

여기서 Secret key 란, 암호화에 사용된 비밀키를 말한다. JWT는 공개키-비밀키 암호화 방식을 사용하는데, 비밀키로 암호화된 정보는 비밀키로 해독할 수 없고, 공개키로 암호화된 정보는 공개키로 해독할 수 없다. 하지만 비밀키로 암호화된 정보는 공개키로 복호화할 수 있고, 공개키로 암호화된 정보는 비밀키로 복호화할 수 있다.

이 방식이 이해되지 않더라도, JWT 생성 및 복호화는 라이브러리로 가능하다. 그리고 JWT 토큰의 특징을 알면, 많이 쓰이는 이유를 알 수 있다.

📌 JWT 토큰의 특징 3가지

  • 웹 표준 (RFC 7519)
    JWT는 웹 표준으로, 대부분의 주류 프로그래밍 언어에서 지원된다.
    (C, Java, Python, C++, R, C#, PHP, JavaScript, Ruby, Go, Swift 등)
  • self-contained
    JWT는 필요한 정보를 자체적으로 모두 지니고 있다.
  • 전달 용이성
    JWT는 JSON 객체로, 가볍고 쉽게 전달 가능하다. 웹서버의 경우 HTTP의 헤더에 넣어서 전달할 수도 있고, URL의 파라미터로 전달할 수도 있다.

여기서 JWT 토큰의 허점을 찾을 수 있다. 결국 세션 기반 인증의 단점을 극복하고자 토큰 기반 인증을 쓰자는 건데, 필요한 정보를 자체적으로 모두 지니고 있으면 위험하지 않은가? 유저 정보가 헤더에 실려 매 요청마다 전달될 텐데?

탈취된다 하더라도, 토큰이 만료될 때까지는 해커가 유저 토큰을 맘대로 쓴다 한들, 막을 수 있는 방법이 없다. 때문에 Access Token, Refresh Token으로 총 두 개의 토큰을 함께 쓰는 방식이 고안되었다.

🔑 Access Token, Refresh Token 으로 인증하기

인증 로직은 다음과 같다. JWT 토큰을 2개 생성하여 토큰 하나(accessToken)는 유효 기간을 짧게, 하나(refreshToken)는 그보다 길게 잡아 보안을 강화하려는 것이다.

accessToken은 매 요청 시, refreshToken은 토큰 재발급 요청 시 accessToken과 함께 보내는 용도로 사용된다. 재발급을 위해서는 refreshToken이 필요하기 때문에, accessToken이 탈취되면 refreshToken을 없애버리면 되는 것이다.

  1. [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
  2. [ 서버 ]는 유저 정보를 검증한 후, 정확하다면 [ 클라이언트 ]에게 accessToken, refreshToken 을 발급해 준다
  3. [ 클라이언트 ]는 전달받은 토큰을 안전한 곳에 저장한다
  4. [ 클라이언트 ]는 매 요청 시 저장된 accessToken을 헤더에 담아 [ 서버 ]에 전달한다
  5. [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다
  6. [ 서버 ]는 accessToken 만료 시 [ 클라이언트 ]에게 에러 메세지를 보낸다
  7. [ 클라이언트 ]는 accessToken, refreshToken을 헤더에 담아 [ 서버 ]에 재발급 요청을 보낸다
  8. [ 서버 ]는 accessToken, refreshToken 을 검증한 후 재발급하여 [ 클라이언트 ]에게 전달한다
  9. [ 클라이언트 ]는 전달받은 새로운 토큰으로 갱신 후 저장한다

하지만 이 방법도 허점이 있다. refreshToken이 탈취되면 막을 방법이 없다. 아쉽게도 여기까지만 공부를 한 상태다.

마치며

이해하는 데만 며칠을 쏟아부은 개념이었는데, 또 토큰 인증으로 로그인 로직을 작성할 일이 생기면 그땐 덜 헤맬 것이다.
이번 프로젝트에서는 재발급 시 refreshToken도 무조건 재발급 해주게끔 만들었는데, 이렇게 되면 사실상 refreshToken의 유효 기간이 큰 의미가 없어진다. 서버 관리의 효율성을 챙기면서, 이용자의 편의성을 놓치지 않으면서, 보안 이슈도 잘 잡아낸다는 건, 적정선을 찾아내는 문제가 될 것 같다.

🙏 참고 자료

참고 - 쿠키와 세션의 차이
참고 - 프론트에서 안전하게 로그인 처리하기
참고 - JWT에 대하여
참고 - JWT 토큰 인증에 대한 소개
참고 영상 - 인증과 인가
참고 영상 - 세션/토큰/JWT
참고 영상 - 세션/쿠키/캐시

profile
좋은 개발자, 좋은 사람

0개의 댓글