전 편에서 사용자 인증 방식으로 Token 기반 방식인 JWT를 이용해 로그인 기능을 개발했다. 그리고 멘토님께 받은 피드백으로 글을 마무리했다. 이번 글에서는 멘토님께 받은 피드백을 계기로 JWT를 이용한 로그인 기능을 고도화하면서 고민한 내용을 공유하려고 한다.
Jwt 반환 방식
처음 개발한 JWT 로그인에서는 발급한 JWT를 어떻게 클라이언트로 보낼지 고민하지 않고 헤더에 'token'이라는 이름으로 반환했다. 이 부분을 멘토님께 피드백을 받고 관련 내용을 공부하면서 적합하다고 생각하는 방식으로 수정했다.
JWT를 내려주는 방식으로 응답 메시지의 바디, 헤더, 쿠키로 3가지 선택지가 존재한다. 선택에 있어 중요한 부분은 JWT를 클라이언트가 안전하게 보관할 수 있을지다. 선택을 위해 3가지 방법의 개념과 특징을 공부했다.
응답 메시지 바디, 헤더
중요한 부분은 응답 메시지 바디 또는 헤더로 JWT를 반환하면 클라이언트가 어디에 JWT를 저장하는지다. 보통 Local Storage, Session Storage에 저장한다. 그런데 여기에 문제가 있다. Local Storage, Session Storage는 브라우저에서 JS통해 접근할 수 있기 때문이다. 즉, XSS 공격에 취약하다.
쿠키
쿠키로 반환하면 클라이언트는 쿠키 저장소에 JWT를 저장한다. 이 방법도 문제가 있다. 브라우저에서 JS를 통해 쿠키 저장소에 접근할 수 있기 때문이다. 즉, XSS 공격에 취약하다. 하지만 쿠키는 HttpOnly 기능을 제공해 준다. 이를 이용하면 브라우저에서 JS를 통해 JWT에 접근할 수 없다. 따라서 XSS 공격을 어느 정도 방어할 수 있다. 하지만 아직 문제가 남았다. 쿠키는 CSRF 공격에 취약하다. 왜냐하면 쿠키는 요청시 자동으로 헤더에 포함되기 때문이다. 하지만 쿠키는 이를 대비해주는 기능도 있다. SameSite 기능을 이용하면 CSRF 공격을 완벽하지는 않더라도 어느 정도 수준에서 예방할 수 있다.
선택
나는 쿠키로 반환하는 방법을 선택했다. 왜냐하면 쿠키는 JWT를 안전하게 지켜줄 부가적인 기능이 존재하기 때문이다. (참조)
Access Token, Refresh Token
JWT 반환 방식으로 쿠키를 선택했다. 하지만 아직도 JWT 탈취를 위험성은 남아있다. 따라서 탈취당하는 경우 보안 문제를 어떻게 해결할지 고민했다. 이는 JWT 만료 기간을 짧게 설정하면 탈취당했을 때의 피해를 최소화할 수 있다. 하지만 단점으로 사용가 자주 로그인해야 하는 문제가 있다. 보안 측면에서 안전하더라도 사용자가 이용하기 불변한 소프트웨어는 매력적이지 않다.
고민과 추가적인 학습을 통해 JWT 탈취 위험성과 사용자의 편의성 문제를 해결하기 위해 나온 아이디어를 찾았다. 바로 refresh token이다. refresh token은 기존 JWT를 재발급받을 수 있는 용도의 토큰이다. 좀 더 자세한 흐름은 아래 그림과 같다.
access token이 만료되면 refresh token을 이용해 재발급받는다. 그리고 access token을 이용해 원하는 요청을 수행한다. 이렇게 하면 사용자가 로그인이 계속 유지되게 느끼도록 개발할 수 있다.
하지만 이 방법에 한 가지 의문이 있었다. refresh token을 탈취당하면 access token의 짧은 만료 기간은 의미가 없을 것 같았다. 그래서 관련된 글을 찾아봤다.
refresh token이 탈취당하면 access token의 짧은 만료 기간은 의미 없어질 수 있다. 하지만 토큰 재발급 로직에 안전장치를 추가하면 위험성을 낮출 수 있다. 생각할 수 있는 예시로는 해외 IP 주소에서 요청이 들어오거나, 계정 도용으로 신고된 아이디인지 검증하는 로직을 추가할 수 있다. 추가로 재발급이 성공할 때마다 refresh token도 재발급해 주면 refresh token의 탈취 위험성을 낮출 수 있다. (참조)
하지만 사용자 편의성을 유지하는 방법으로 refresh token을 도입하는 게 유일한 방법은 아니다. refresh token 없이 access token의 만료 기간은 길게 유지한 채 db를 이용해 access token을 세밀하게 제어할 수 있다. 하지만 해당 방법은 refresh token을 이용하는 것 보다. 더 자주 서버의 db에 접근해야 한다. 그러면 서버의 확장성은 자연스럽게 떨어지게 된다. (참조)
나는 서버 확장성 이유로 토큰 기반 방식을 선택했기에 refresh token을 이용해 사용자 편의 문제를 해결하는게 적절하다고 생각했다. 그래서 refresh token을 도입했다.
기존 로그인 기능에 Refresh Token 도입하기
기존 Jwt 모듈은 access token 하나만 발급했다. 따라서 refresh token을 발급해주는 기능을 추가해줬다.
Jwt 모듈 코드 일부
기존 로그인 방식은 응답 메시지의 token 헤더에 access token을 반환해 줬다. 이를 쿠키를 이용해 반환하도록 그리고 refresh token도 반환하도록 수정했다.
UserController 코드 일부
refresh token의 만료 기간은 길어서 탈취당하면 큰 피해를 볼 수 있다. 이를 방지하기 위해 refresh token을 이용해 access token을 재발급하는 과정에서 refresh token도 재발급해 줬다.
JwtService 코드 일부
마무리
멘토님의 피드백을 바탕으로 보안을 위해 JWT를 쿠키를 이용해 반환하고 만료 시간을 짧게 설정했다. 하지만 보안을 생각하다 보니 사용자 편의성에 문제가 생겼고 이를 refresh token을 이용해 해결했다. 이번 경험을 통해 '개발자가 수고스러워야 사용자가 편리하다.'라는 말이 떠올랐다. 소프트웨어는 사용자의 의미 있는 경험을 위해 존재하기에 사용자의 편의를 위해 노력하는 개발자가 되어야겠다.
사용자 인증, 인가 기능이 첫 번째 버전보다 많이 좋아졌다. 하지만 한 가지 아쉬움이 남는다. 현재는 refresh token이 db에 저장되는데 refresh token에 대한 IO 연산은 자주 발생한다. 따라서 성능에 문제가 생길 수 있을 것 같다. 이를 보완할 방법에 대해 고민해 봐야겠다.
'개발 경험기' 카테고리의 다른 글
[JWT 로그인 구현기] (3) Redis 도입하기 (3) | 2022.07.16 |
---|---|
[JWT 로그인 구현기] (1) Session vs Token (0) | 2022.07.12 |