JWT를 이용한 인증 + Redis를 이용한 로그아웃

박준수·2023년 12월 20일
0

이것저것

목록 보기
2/9
post-thumbnail

사전지식

  • HTTP는 Stateless(무상태성) 하다!

→ 서버는 이전 요청과 현재 요청의 관계가 없다고 판단한다.

🔑인증/인가 을 하는 방법

1. Request Header을 이용하는 방법

  • 요청이 들어온 url을 junsu:qwer1234@ 파싱 후 Base64라는 인코더를 이용해서 인코딩을 한 후에 http request를 전달 (이 부분은 클라이언트. 즉, 브라우저가 한다)
  • url에서 로그인에 관한 부분을 파싱한 후에 인코더를 통해서 인코딩한 문자열을 가지고 있음(브라우저가)
  • 이를 request header의 Authorization에 넣어서 보내주는 개념.
  • 이렇게 브라우저가 http를 통해 서버로 날려주면 서버에서는 DB 체킹을 하고 일치여부를 판다.

  • 문제점 : http는 stateless이기에 매번 인증해야 한다.(인증이 필요한 모든 요청에서는 1번 상황을 반복해서 사용해야 함)
    • 문제를 해결하기 위해 브라우저의 Strorage를 사용해야 함 (로컬 스토리지, 세션 스토리지, 쿠키)
    • 쿠키를 사용하면 쿠키에 사용자 id, pw를 집어 넣어서 사용자가 인증이 필요한 요청을 할 때 이 쿠키를 같이 넣어서 보내준다.
    • 그러나 다시 문제점 발생 : 해커한테 쿠키에 사용자의 중요 데이터를 너무 쉽게 노출하게 됨, 클라이언트가 서버보다 보안에 취약하다는 단점!

2. Session 활용하기

  • 세션은 인증된 사용자의 식별자(pk 등)와 랜덤한 문자열로 '세션 id'를 만들어서 이를 response header로 넘겨주고 이를 클라이언트가 저장할 수 있도록 한다.
  • DB에서 체킹 -> 서버로 {session id : 사용자 식별자}를 응답 해더로 보냄. 클라이언트측에서는 {id: session id} 를 저장
  • 장점 : 클라이언트가 사용자의 중요 데이터를 raw한 상태로 가지고 있지 않기 때문에 해커가 정보를 가져가더라도 크게 위험이 없음, 세션의 만료기간을 지정할 수 있다는 것이다. → 만료가 지난 세션은 해커가 가져가더라도 유효하지 않은 정보가 됨. 세션 관리를 서버 자체에서 하기 때문에 만일 탈취가 된 세션을 서버에서 삭제해버리면 세션 자체를 이용하지 못하게 되어서 보안이 향상!

  • 문제점 : 로드밸런서를 이용해 서버를 여러 대 두는 상황에서는 처음 인증을 처리했던 한 대의 서버에서만 그 사용자에 대한 세션 정보를 가지고 있기 때문에 다음 요청에서 그 서버로 요청이 오지 않는다면 문제가 발생하게 됨

  • 해결 방법
    • 세션 스토리지를 사용하게 되면 로드밸런서가 어떤 서버로 요청을 보내더라도 결국에는 모두 하나의 세션 스트로지로 요청이 들어오기 때문에 문제를 피할 수 있다.

  • 그러나 클라이언트가 많아진다면 세션 스토리지가 터진다는 또 다른 문제점이 발생한다.
  • 각각 클라이언트, 서버, 세션 스토리지(세션 저장소)에서 모두 한 번씩 사용자의 정보를 관리할 수 있게 했더니 문제가 발생했다.
  • 클라이언트, 서버, 세션스토리지가 통신할 때 사용하는 http와 서버 자체가 지향하는 rest api가 무상태성을 기초로 하는데, 인증/인가를 할 때는 사용자의 정보, 사용자의 상태를 가지고 있어야 한다. 무상태와 상태성 → 두 개념이 충돌.

3. JWT를 사용하여 인증/인가 하는 방식

  • 클라이언트, 서버에 사용자의 정보를 저장하는게 아닌 http 요청과 응답 안에 사용자 상태를 담아보자 → 사용자의 인증/인가를 처리하는게 토큰을 활용한 인증 인가 방식이다.
  • 간단한 JWT 소개 :
    • JWT는 시크릿 키를 이용해서 만든다.
    • JWT 자체는 해독하기 쉬우므로 민간한 정보(비밀번호 등)를 담지 않는다.
    • 시크릿 키가 노출이 안되게 하는 것이 중요하기 때무에 서버 내부에서 잘 관리해야 한다.
  • 토큰을 이용한 인증 프로세스
    • 로그인 요청 → DB 체킹 후 일치하면 시크릿 키 이용해 토큰 생성 → 클라이언트가 엑세스 토큰을 저장해둠 → 이 토큰을 이용해서 요청/응답
    • 서버는 토큰에 대한 유효성 검사를 시크릿 키로 진행 → 유효하지 않으면 버리고 유효하면 사용자 정보(사용자 이름 , 만료 시기, 권한 등)를 파악
    • AccessToken 만료시간에 도달했으면 RefressToken을 통해 다시 토큰 재발급 → 사용자는 토큰이 만료된지 모른채 계속 서비스를 사용 가능
  • 장점 : 로드밸런서를 이용한 서버에서도 문제가 없음. 각 서버에서 시크릿 키만 가지고 있으면 되기 때문. 서버가 확장되더라도(여러대가 추가되더라도) 문제가 없음. 각자 해독을 해서 인증을 하면 됨. 즉, 토큰으로 상태관리를 하기에 따로 세션을 둘 필요가 없다. 효율성이 좋아지고 DB를 찔러도 되지 않기 때문에 속도가 빠르다는 장점이 있다.

  • 단점 : AccessToken을 탈취 당하면 해킹의 위험이 됨
  • 따라서 AccessToken의 유효기간을 짧게 설정하는데, 정상적인 클라이언트는 유효기간이 끝난 Access Token에 대해 Refresh Token(새로운 Access Token을 발급하기 위한 토큰) 을 사용하여 새로운 Access Token을 발급받을 수 있다.
  • 그런데 만약 Refresh Token이 유출되어서 다른 사용자가 이를 통해 새로운 Access Token을 발급받았다면? 이 경우, Access Token의 충돌이 발생하기 때문에, 서버측에서는 두 토큰을 모두 폐기시켜야 한다. 국제 인터넷 표준화 기구(IETF)에서는 이를 방지하기 위해 Refresh Token도 Access Token과 같은 유효 기간을 가지도록 하여, 사용자가 한 번 Refresh Token으로 Access Token을 발급 받았으면, Refresh Token도 다시 발급 받도록 하는 것을 권장하고 있다.

🤔JWT를 이용한 인증/인가 방식을 적용한 이유

  • Request Header을 이용하는 방법은 보안상 너무 취약함으로 사용하면 안될 것 같다.
  • 세션은 항상 인증 요청을 할 때마다 세션 ID를 세션 저장소에 있는 세션 ID에 비교를 해야 한다. 30분에 1만번의 요청을 한다고 했을 때 I/O는 1만 번 동작해야 한다.
  • 즉, 쿠키와 세션을 이용한 방법은 서비스가 확장될 경우 세션 스토리지에 과부하가 올 수 있다.
  • 대신, JWT를 이용하면 토큰이 상태를 가지고 있으므로 요청을 할 때마다 인증을 확인하는 추가적인 I/O 요청이 발생하지 않는다. Access Token의 유효시간을 짧게 하여 최소한의 보안성을 보장해준다. (Access Token의 만료 시간이 30분이면 30분당 1번씩 재발급 요청을 하면 된다.)

🤔JWT를 이용하여 로그아웃 하는 방법

  • JWT를 통해 로그인을 하는 방법을 적용하였고, 로그아웃을 하는 방법을 찾아보아야 했다.
  • 토큰을 만료시키면 로그인이 안 되는 것이니 어떻게 토큰을 만료시키면 좋을지 찾아보았다.
    1. 클라이언트 Storage에 저장된 토큰 제거

      → 로그아웃 할 경우 프론트엔드 단에서 Storage에 있는 JWT를 Clear 하면 된다. 그러나 이 방법은 만약 유저가 토큰을 미리 카피 했더라면 로그아웃을 성공 했더라도 계속해서 서버에 요청을 보낼 수 있게 된다. (참고 : 한 번 발급된 토큰은 수정을 할 수 없다. 즉, 만료시간을 수정하지 못한다.)

    2. 블랙리스트 생성

      → 로그아웃 하고 싶은 토큰들을 블랙리스트에 모은다. 그리고 블랙리스에 토큰이 들어오면 해당 토큰을 무효화하는 작업을 진행하는 방법이다.

🤔블랙리스트로 Redis를 사용해야 하는 이유

  • 일단 클라이언트 Storage에 저장된 토큰을 제거하는 방식은 보안 상에 안 좋을 것 같아 안될 것 같다.
  • 기존에 토큰이 만료되었을 시 JWT에서 Refresh Token을 이용하여 재발급을 해 주는 방식을 알아보자
    1. 클라이언트가 ID, PW로 서버에게 인증을 요청하고 서버는 이를 확인하여 Access Token과 Refresh Token을 발급합니다.
    2. 클라이언트는 이를 받아 Refresh Token를 본인이 잘 저장하고 Access Token을 가지고 서버에 자유롭게 요청합니다.
    3. 요청을 하던 도중 Access Token이 만료되어 더 이상 사용할 수 없다는 오류를 서버로부터 전달 받습니다.
    4. 클라이언트는 본인이 사용한 Access Token이 만료되었다는 사실을 인지하고 본인이 가지고 있던 Refresh Token를 서버로 전달하여 새로운 Access Token의 발급을 요청합니다.
    5. 서버는 Refresh Token을 받아 서버의 Refresh Token Storage에 해당 토큰이 있는지 확인하고, 있다면 Access Token을 생성하여 전달합니다.
    6. 이후 2로 돌아가서 동일한 작업을 진행합니다.
  • Refresh Token Storage는 서버에서 Refresh Token을 저장하는 저장소이다.
    • 사실상 세션과 별반 차이 없이 특정 Storage에 I/O작업이 발생하게 되기 때문에 Access Token이 지속되는 짧은 시간동안만 I/O작업이 일어나지 않는다이지, 세션의 단점을 하나 가져가는 셈이 됩니다. (물론 위에서 언급 했듯이 예를 들어 30분에 세션은 항상 인증 요청을 해야하지만 JWT는 1번만 인증 요청을 보네긴 함)
  • 그렇다면 Refresh Token을 저장할 Refresh Token Storage 이 필요한데, RDBMS에 저장하면 Refresh Token의 만료 시간(Time To Live)를 통해 주기적으로 삭제해야 하는 번거로움이 생긴다.
  • 해결 방법으로 Redis의 In-Memory DB를 사용하여 RDBMS보다 더 빠르게 조회가 가능하고, 만료시간을 설정하여 저장할 수 있다. 즉 Redis에 있는 데이터를 조회하여 로그아웃을 구현할 수 있다.

구현 방법

로그인/로그아웃

  1. 로그인 했을 때는 Key로 email, Value는 RefreshToken을 Redis에 저장. (만료시간은 RefreshToken 유효시간 만큼) → 클라이언트로부터 엑세스 토큰과 리프레쉬 토큰을 발급
  2. 로그아웃 했을 때는 AccessToken으로부터 Key(email)를 받아와서 Redis에 삭제 → 해당 AccessToken은 Key, Value “Logout”으로 Redis에 저장 (만료시간은 AccessToken의 남은 시간)
    • JwtAuthenticationFilter 에서 로그인 되어있으면 토큰으로부터 유저 정보를 받아와서 SecurityContext 에 Authentication 객체를 저장한다.

재발급

  • Redis에 RefreshToken이 저장 되어있을 때
    • AccessToken이 만료되었을 시 401 UnauthorizedTime 예외가 발생 → Redis에 있는 RefreshToken으로 토큰 재발급
  • Redis에 RefreshToken이 저장 안 되어있을 때
    • AccessToken 만료 → 401 예외 → 재발급 → Redis에 RefreshToken이 없기에 403 예외 → 로그인 페이지로 이동 후 다시 로그인

→ 참고자료에서 spring boot로 구현하는 방법은 자세히 나와있다.

참고 자료

[10분 테코톡] 🎡토니의 인증과 인가

[Spring] Spring Security + JWT 토큰을 통한 로그인

JWT(Json Web Token) 인증방식

SpringBoot + JWT를 이용한 로그아웃

profile
방구석개발자

0개의 댓글