조급하면 모래성이 될뿐

JWT 로그인 구현 본문

TroubleShooting/Yapp

JWT 로그인 구현

Pawer0223 2023. 7. 24. 02:29

현재 진행하는 프로젝트에서는 소셜로그인을 사용하지 않는다. 자체 인증시스템이 필요했고 JWT를 적용했다.

 

기존의 인증방식

기존의 쿠키나 세션방식은 서버 또는 클라이언트에 유저 정보를 저장하여 인증을 수행한다. 쿠키는 웹 브라우저에 저장하기 때문에 비교적 쉽게 탈취당할 수 있다. 

세션은 다중서버구현 시 정합성을 보장할 수 없다. A클라이언트의 요청을 S1서버에서 인증했을 때, 두 번째 요청이 S2로 전송되면 S2세션에는 A유저의 정보를 모르기 때문에 인증이 되지 않는다. (스티키 세션, 세션 클러스터링, 메모리 디비를 사용해 해결할 순 있다.)

 

JWT

JWT는 RFC 7519 버전에 만들어진 약속이다.  JWT의 핵심은 서명된 토큰이라는 것이다. 쉽게 서버에서 지정한 SecretKey로 JWT토큰을 만들어서 클라이언트에 발급한다. 이후 JWT 토큰을 통해 요청을 받아 서버의 SecretKey로 복호화가 성공한다면 내가 발급한 토큰이 맞다고 믿고 쓰는 거다.

 

JWT의 또 다른 주요 특징은 클레임(claim)이라는 정보를 토큰 자체에 포함하고 있다는 것이다.(Self-Contained) 클라이언트가 서버에 요청을 보낼 때, 토큰을 포함하여 인증정보를 전송하면 서버는 토큰을 디코딩하여 클레임을 확인하고 인증을 수행한다.

 

JWT를 사용해서 기존의 쿠키나 세션방식의 단점을 보완할 수 있다. 쿠키에 민감정보를 노출하지 않고, PK 등을 클레임(claim)에 포함시키면 서버에서는 JWT 검증 및 인증을 수행한다.

 

물론 JWT에 민감정보를 포함시킨다면.. 의미가 없다. 이유는 JWT는 약속된 규칙이기 때문에.. JWT에 포함된 데이터는 쉽게 읽을 수 있기 때문이다. JWT는 header, payload, signature를 가지게 되는데 payload에 필요한 유저정보를 담을 수 있다. JWT를 이해하기 위한 포스팅이 아니기 때문에.. 자세한 설명은 참조한 블로그 링크로 대체한다.

- https://velopert.com/2389

 

JWT에 포함된 데이터를 쉽게 읽을 수 있다는 내용을 간단히만 정리하면 https://jwt.io 사이트에 접속해서 발급된 토큰을 넣으면 해당 JWT 내용을 해독할 수 있다.

 

- Encoded: 서버에서 JWT토큰을 발급하면 저런 문자열로 발급된다. 점(.)으로 header, payload, signature를 구분할 수 있다.

- Decoded: Jwt를 복호화한 모습이다.

 

특정 서버에 세션을 저장할 필요가 없기 때문에 세션정합성 문제도 해결할 수 있다. 즉, Stateless 하게 인증이 가능하기 때문에 분산환경에서도 적용할 수 있다.

 

JWT 주의점

JWT가 탈취되었다면?? 서버는 토큰 사용자가 올바른 클라이언트지 구분할 수 있을까??

뭐.. 어떻게든.. 할 순 있겠지만..? 일반적으로는 인증로직을 그렇게까지 하지는 않는 것 같다..

 

쨋든 JWT 문제점은, 탈취될 경우(쿠키 방식과 동일하게) 보안 취약점이 존재한다. 만약 JWT 만료기한을 주지 않는다면??

-> JWT가 한번 탈취되면 누구든 이 토큰을 가지고 인증을 할 수 있다.

 

때문에 일반적으로는 JWT 만료시간을 짧게 준다. (일반 쿠키 방식과의 차이점, 기한이 짧은 서명 된 토큰) 그러면 탈취돼도 유효기간이 지나면 못쓰는 토큰이 되니깐... 근데 이러면 사용자는 엄청 귀찮다. JWT를 다시 발급받으려면 로그인을 또 해야 한다. 만료시간이 10분이면.. 10분마다 로그인을 계속해야 쓸 수 있다.

 

AccessToken, RefreshToken

계속 로그인해야 하는 문제를 2개의 JWT토큰을 사용해서 보완한다. AccessToken은 인증을 하기 위한 토큰, RefreshToken은 AccessToken을 재발급하기 위한 토큰이다.

 

AccessToken은 만료기한을 짧게 준다. RefreshToken은 만료기한을 길게 준다. AccessToken이 만료되면 RefreshToken을 가지고 요청해서 다시 AccessToken을 발급받을 수 있다. 이러면 사용자는 RefreshToken 만료기한까지는 계속해서 AccessToken을 재발급받을 수 있기 때문에, 여러 번 로그인하지 않아도 된다.

 

하지만 RefreshToken은 Stateless 하게 관리하지 않는다. 보통 Redis와 같은 저장소를 사용한다. 굳이.. 저장소를 써야 하는 이유는 내가 느끼기엔 RefreshToken 도 자주 재발급해주려고.. 그러는 것 같다. RefreshToken은 보통 만료기한을 길게 잡다 보니.. 위에서 언급한 탈취되었을 때 취약점이 존재할 수밖에 없다..

 

때문에 RefreshToken은 만료기한이 남아있더라도 재발급 요청을 받으면 항상 새롭게 발급한다. 이때, JWT는 문자열 자체로 존재하기 때문에 발급한 토큰의 만료기한을 전역적으로 변경할 수 없다. 그래서 별도 저장소를 사용해서 이 유저가 현재 가진 RefreshToken이 무엇인지 저장하고, 식별하는데 장점이 있는 것 같다.

 

key = userId,

value = refreshToken

 

이렇게 refreshToken을 관리하게 된다면 사용가능한 refreshToken은 1개뿐이다. 그렇지 않다면 만료되지 않은 refreshToken이 여러 개 생겨나게 될 수 있고... 결과적으로 토큰을 재발급받을 수 있는 확률이 높아질 것이다.

 

How?

인증로직 자체는 심플하다. (물론 구현은 쉽지 않았지만..)

 

1. 로그인 요청이 오면 JWT 토큰으로 accessToken, refreshToken을 발급한다.

- accessToken 만료시간은 1시간, refreshToken 만료시간은 하루이다.

- refreshToken은 redis에 저장한다.

 

2. accessToken이 만료되면 클라이언트는 accessToken과 refreshToken을 가지고 재발급을 요청한다.

- 재발급 로직에 accessToken을 포함시키는 이유는 크게 2가지다.

- 첫번째는 accessToken의 만료기한이 남아있는 경우는 재발급하지 않기 위해서다. 위에서 언급한 문제와 동일하게 JWT는 문자열 자체로 존재하기 때문에 발급한 토큰의 만료기한을 전역적으로 변경할 수 없다. 재발급 요청이 올 때마다 accessToken을 재발급하게 된다면 동시에 접근할 수 있는 accessToken이 많아진다는 의미다. (잠깐의 시간일지라도.)

- 두번째는 refreshToken만 탈취된 경우를 대비하기 위함이다. 재발급은 인증된 만료기한이 지난 accessToken 없이는 재발급을 받을 수 없다.

 

3. 재발급 시 accessToken, refreshToken 모두 재발급한다.

- 위에서 언급한 이유로.. refreshToken도 자주 갱신을 해주어서 보안을 강화한다. 

 

 

다음으로 고민한 점은 이 토큰을 클라이언트에게 어떻게 발급해 주고, 통신할 것인지에 대한 문제이다.

반응형