jwt 토큰을 이용한 인증에 대한 이해를 다뤘습니다. 공부 중이라 잘못된 부분이 있을 수 있으니, 지적해 주시면 감사드리겠습니다 🙏
Spring boot와 React로 간단한(간단하지 않은) 쇼핑몰을 만드는 팀 프로젝트를 진행 중에 있는데, JWT 토큰을 이용한 로그인을 구현하기로 했다. 관련 학습 자료의 양은 방대했지만 이해하기까지 꽤 오랜 시간이 걸렸다. 잊어버리기 전에 정리하기 위해 글을 쓴다.
인증 (authentication) : 인증을 하고 로그인을 하는 것(가입된 유저임을 인증)
인가 (authorization) : 인증된 사용자에게 허가해 주는 활동(ex. 마이페이지 정보 조회)
참고 - 쿠키와 세션의 차이
기존의 인증 방식은 서버(세션) 기반 인증이다. 서버 기반 인증 로직은 다음과 같다.
- [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
- [ 서버 ]는 세션을 생성, 저장한다
- [ 서버 ]는 세션 id 를 쿠키에 담아 [ 클라이언트 ]에게 전달한다
- [ 클라이언트 ]는 요청 시 세션 id 를 쿠키에 담아 [ 서버 ]에 전달한다
- [ 서버 ]는 전달받은 쿠키의 세션 정보와, DB에 저장된 세션 정보를 비교한다
- [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다
이는 서버에서 유저 정보를 저장, 관리하기 때문에 매 요청 시마다 서버와 통신하게 된다.
때문에 다음과 같은 문제가 생길 수 있다.
결국 토큰 기반 인증은, 서버에서 유저 정보를 관리하지 않기 위한 대안이라고 보면 된다.
서버 기반 인증 시스템의 단점을 극복한, 무상태와 확장성이 핵심이다. 유저 정보를 서버나 세션에 담아두지 않고, 토큰에 담아 전달하기 때문에, 서버에서는 유저 정보를 저장하거나 관리할 필요가 없고, 서버 컴퓨터가 여러 대일지라도 큰 어려움 없이 인증/인가를 해줄 수 있다.
그리고 토큰에는 유효 기간이 있다. 만료 시간은 정하기 나름인데, 만료 시간이 짧으면 보안이 강화되지만 서버와의 통신이 잦아진다는 부담이 있다. 만료 시 재발급 로직도 정하기 나름인데, 여기서는 서버에서 토큰 만료를 먼저 알아챈 후 에러 메세지를 보내면, 클라이언트가 재발급 요청을 수행하는 로직으로 만들었다.(axios interceptors 사용)
토큰 기반 인증 로직은 다음과 같다.
- [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
- [ 서버 ]는 유저 정보를 검증한 후, 정확하다면 [ 클라이언트 ]에게 토큰을 발급해 준다
- [ 클라이언트 ]는 전달받은 토큰을 안전한 곳에 저장한다
- [ 클라이언트 ]는 요청 시 저장된 토큰을 헤더에 담아 [ 서버 ]에 전달한다
- [ 서버 ]는 전달받은 토큰을 검증한다
- [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다
- [ 서버 ]는 토큰 만료 시, [ 클라이언트 ]에게 에러 메세지를 전달한다
- [ 클라이언트 ]는 [ 서버 ]에 토큰 재발급 요청을 보낸 후, 새로 발급받은 토큰으로 갱신 후 저장한다
대충 보면 이해되는 듯 하나, 자세히 파고들면 의문이 생긴다.
차례대로 하나씩 풀어보려 한다.
참고 - 프론트에서 안전하게 로그인 처리하기
결론부터 말하면, 웹 스토리지와 쿠키에 저장할 수 있고, 쿠키에 저장하는 것이 보다 더 안전하다. 클라이언트단에 저장되는 정보는 보안에 취약하기 때문에, 서버에서 어느 정도 제어할 수 있는 쿠키에 저장하는 방식이 더 추천된다. 하지만 어디든 토큰이 탈취되지 않으리라는 보장은 없기 때문에, 암호화하여 저장하는 것이 좋다. 비교 요약은 다음과 같다.
- 👍 브라우저 로컬 저장소를 이용하는 것으로, 구현하기 쉽다
- 👍 하나의 도메인에 국한되지 않으며, 도메인 당 최대 5~10MB까지 저장 가능하다
- 💩 스크립트 공격에 취약하다(XSS 공격)
해결
: 해킹에 사용될 수 있는 코딩에 사용되는 입출력 값을 검증하여 무효화시킴
- 👍 httpOnly 로 쿠키를 설정하면, XSS 해킹 문제를 해결할 수 있다
- 👍 Secure 옵션을 주면 https 로만 쿠키가 전송되기 때문에, 보안을 강화할 수 있다
- 💩 최대 4MB까지 저장 가능하다
- 💩 한정된 도메인에서만 쿠키를 사용할 수 있다
해결
: 토큰이 필요할 때 기존 토큰으로 새 토큰을 받아올 수 있도록 api 설계- 💩 CSRF 공격 위험이 있다.
해결
: 허용한 url에서만 요청을 받도록 함
이에 대한 의문은 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 토큰의 특징을 알면, 많이 쓰이는 이유를 알 수 있다.
- 웹 표준 (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으로 총 두 개의 토큰을 함께 쓰는 방식이 고안되었다.
인증 로직은 다음과 같다. JWT 토큰을 2개 생성하여 토큰 하나(accessToken)는 유효 기간을 짧게, 하나(refreshToken)는 그보다 길게 잡아 보안을 강화하려는 것이다.
accessToken은 매 요청 시, refreshToken은 토큰 재발급 요청 시 accessToken과 함께 보내는 용도로 사용된다. 재발급을 위해서는 refreshToken이 필요하기 때문에, accessToken이 탈취되면 refreshToken을 없애버리면 되는 것이다.
- [ 클라이언트 ]가 [ 서버 ]에 로그인 요청을 한다
- [ 서버 ]는 유저 정보를 검증한 후, 정확하다면 [ 클라이언트 ]에게 accessToken, refreshToken 을 발급해 준다
- [ 클라이언트 ]는 전달받은 토큰을 안전한 곳에 저장한다
- [ 클라이언트 ]는 매 요청 시 저장된 accessToken을 헤더에 담아 [ 서버 ]에 전달한다
- [ 서버 ]는 성공/실패 여부를 [ 클라이언트 ]에게 전달한다
- [ 서버 ]는 accessToken 만료 시 [ 클라이언트 ]에게 에러 메세지를 보낸다
- [ 클라이언트 ]는 accessToken, refreshToken을 헤더에 담아 [ 서버 ]에 재발급 요청을 보낸다
- [ 서버 ]는 accessToken, refreshToken 을 검증한 후 재발급하여 [ 클라이언트 ]에게 전달한다
- [ 클라이언트 ]는 전달받은 새로운 토큰으로 갱신 후 저장한다
하지만 이 방법도 허점이 있다. refreshToken이 탈취되면 막을 방법이 없다. 아쉽게도 여기까지만 공부를 한 상태다.
이해하는 데만 며칠을 쏟아부은 개념이었는데, 또 토큰 인증으로 로그인 로직을 작성할 일이 생기면 그땐 덜 헤맬 것이다.
이번 프로젝트에서는 재발급 시 refreshToken도 무조건 재발급 해주게끔 만들었는데, 이렇게 되면 사실상 refreshToken의 유효 기간이 큰 의미가 없어진다. 서버 관리의 효율성을 챙기면서, 이용자의 편의성을 놓치지 않으면서, 보안 이슈도 잘 잡아낸다는 건, 적정선을 찾아내는 문제가 될 것 같다.
참고 - 쿠키와 세션의 차이
참고 - 프론트에서 안전하게 로그인 처리하기
참고 - JWT에 대하여
참고 - JWT 토큰 인증에 대한 소개
참고 영상 - 인증과 인가
참고 영상 - 세션/토큰/JWT
참고 영상 - 세션/쿠키/캐시