Jazz Meet 프로젝트를 진행하면서 관리자 계정을 어떻게 구현해야 할지 고민한 내용입니다.
⭐ 첫 번째 고민: Cookie & Session vs. JWT
🛠️ Cookie & Session 기반 인증
Cookie와 Session 기반 인증 로직은 다음과 같습니다.
- 사용자 로그인 요청
- 서버에서 인증 처리
1) 받은 아이디와 비밀번호 검증
2) 인증이 성공하면, 서버는 이 사용자에 대한 세션 생성 - 서버는 생성된 세션 ID를 사용자의 웹 브라우저에 쿠키 형태로 전송
- 브라우저가 서버에 요청을 보낼 때마다 세션 ID를 쿠키로 같이 전송
- 사용자가 로그아웃을 요청하면, 서버는 해당 사용자의 세션을 종료(삭제)하고, 사용자의 브라우저에 저장된 쿠키(세션ID)를 무효화
1) 이후 사용자가 다시 인증이 필요한 페이지에 접근하려고 하면, 서버는 유효한 세션 ID가 쿠키에 없기 때문에 사용자를 비인증 상태로 판단
Sesstion & Cookie 기반 인증의 장단점
장점
서버가 사용자의 로그인 상태를 쉽게 관리할 수 있고, 사용자는 여러 페이지를 이동하면서도 로그인 상태를 유지할 수 있습니다.
단점
- 쿠키를 사용하기 때문에 보안상 주의가 필요
- 서버에서 세션 저장소를 사용하므로 요청이 많아질 경우 서버에 부하가 심해짐
세션 데이터를 저장할 때 기본적으로 서버의 메모리에 저장된다. 각 사용자별로 고유한 세션을 생성하고 유지해야 하기 때문에 사용자의 수가 많아질 수록 서버 메모리 사용량도 증가한다.
보완
- HTTPS를 통해 암호화되어 전송해야 하며, 쿠키에 저장된 정보는 민감한 정보를 직접 포함하지 않아야 합니다. 세션 하이재킹 공격에 대비하기 위해 쿠키에 플래그를 설정 하여 보호 조취를 취하는 것이 좋습니다.
- 세션 하이재킹: 공격자가 사용자의 세션 토큰이나 세션 쿠키를 가로채 그 사용자로서 서버에 접근하는 보안 공격
- 방어 플래그: Secure 플래그, HttpOnly 플래그, SameSite 플래그
- 서버 부하 방지
- 세션 스토리지 외부화
세션 데이터를 서버 메모리 대신 외부 데이터 스토리지 시스템(Redis 등)에 저장하여, 서버의 메모리 부담을 줄이고, 서버 간에 세션 정보를 공유할 수 있는 기능을 제공 - 스테이트리스 설계 고려
JWT와 같은 토큰 기반 인증 방식으로 서버 측에서 사용자 상태를 저장할 필요가 없어 서버 부하를 줄일 수 있음
- 세션 스토리지 외부화
🛠️ JWT 기반 인증
JSON Web Token, 인증에 필요한 정보들을 암호화 시킨 토큰을 의미합니다. JWT 토큰을 HTTP 헤더에 실어 서버가 클라이언트를 식별합니다.
JWT의 구조{Header} . {Payload} . {Signature}
- Header : 토큰의 유형(typ, 보통 JWT)과 해싱 알고리즘(alg, 예: HMAC SHA256 또는 RSA)이 포함된 JSON 객체, 헤더는 Base64Url 방식으로 인코딩
- Payload : 토큰에 담을 클레임(claim)이 포함된 JSON 객체, Base64Url 방식으로 인코딩
- Signature : 헤더의 인코딩된 값, 페이로드의 인코딩된 값, 비밀 키를 합친 후 헤더에 명시된 알고리즘을 사용하여 생성, 메시지가 중간에 변경되지 않았음을 검증하는 데 사용
JWT 기반 인증 로직은 다음과 같습니다.
- 클라이언트 로그인 요청이 들어오면, 서버는 검증 후 클라이언트 고유ID 등의 정보를 Payload 에 저장
- 암호화할 비밀키를 사용해 토큰 발급
- 클라이언트는 전달받은 토큰을 저장해두고, 서버에 요청할 때마다 토큰을 요청 헤더
Authorization
에 포함시켜 함께 전달 - 서버는 토큰의 Signature을 비밀키로 복호화한 다음, 위변조 여부 및 유효 기간 등을 확인
- 유효한 토큰이라면 요청에 응답
JWT 기반 인증의 장단점
장점
- Header와 Payload를 가지고 Signature를 생성하므로 데이터 위변조를 막을 수 있습니다.
- 인증 정보에 대한 별도의 저장소가 필요 없습니다.
- 확장성이 우수합니다.
상태 비저장
분산 시스템이나 MSA에서 서비스 간 인증 용이
단점
- JWT는 토큰의 길이가 길어, 인증 요청이 많아질 수록 네트워크 부하가 심해집니다.
- Payload 자체는 암호화되지 않기 때문에 유저의 중요한 정보는 담을 수 없습니다.
- 토큰을 탈취당하면 대처하기 어렵습니다.
유효기간이 만료될 때까지 계속 사용이 가능하기 때문
보완
짧은 만료 기한 설정
토큰이 탈취되더라도 빠르게 만료되기 때문에 피해를 최소화할 수 있습니다.
→ 그러나 사용자가 자주 로그인해야 하는 불편함Refresh Token
1) 클라이언트가 로그인 요청을 보내면 서버는 Access Token 및 그보다 긴 만료 시간을 가진 Refresh Token을 발급
2) 클라이언트는 Access Token이 만료되었을 때 Refresh Token을 사용하여 Access Token의 재발급을 요청
3) 서버는 DB에 저장된 Refresh Token과 비교하여 유효한 경우 새로운 Access Token을 발급하고, 만료된 경우 사용자에게 로그인을 요구
→ Access Token의 만료 기한을 짧게 설정할 수 있으며, 사용자가 자주 로그인할 필요가 없습니다.
→ 검증을 위해 서버는 Refresh Token을 별도의 storage에 저장해야 합니다. 추가적인 I/O 작업이 일어나기 때문에 JWT의 장점을 완벽하게 누릴 수 없습니다. 클라이언트도 탈취 방지를 통해 Refresh Token을 보안이 유지되는 공간에 저장해야 합니다.
🛠️ 선택
세션보다 확장성이 우수한 JWT 방식을 도입하고, 토큰을 Access Token과 Refresh Token으로 나눠서 보안을 강화하기로 결정했습니다.
⭐ 두 번째 고민: Refresh Token을 서버에서 어떻게 관리해야 하는가?
처음에는 MySQL의 유저 테이블에 그대로 저장했으나, Refresh Token이 만료되었는지 주기적으로 확인해야 하는 로직(ex. 스케줄러 사용)이 별도로 필요했습니다. Access Token의 만료 시간이 짧은 만큼 Refresh Token의 db 접근이 많아지기 때문에 MySQL보다 더 빠르고 값에 만료 시간을 줄 수 있는 Redis를 사용하는 것이 좋을 것이라 판단했습니다.
따라서 Refresh Token의 저장소를 MySQL에서 Redis로 변경했습니다.
⭐ 세 번째 고민: Refresh Token 은 브라우저에서 어떻게 관리해야 하는가?
Refresh Token을 탈취 당할 경우 Access Token을 발급하여 사용자인 척 서비스에 접근을 할 수 있기 때문에 접근하지 못하게 쿠키에 다음 플래그를 적용하였습니다.
- secure(true) : HTTPS 환경에서만 사용
- httpOnly(true) : 자바스크립트로 접근 불가능
- sameSite(lax) : 쿠키가 동일한 사이트의 요청 또는 일부 안전한 크로스 사이트에서 쿠키를 받을 수 있음
특히 완전히 다른 도메인으로 등록해두었던 프론트 어드민을 백엔드 어드민의 하위로 변경하여 same site 정책에서 동일한 도메인으로 인식되도록 만들었습니다.
⭐ 네번째 고민: Redis를 사용하여 로그아웃한 유저를 매번 식별한다면 세션 저장소로 Redis를 사용하는 것과 동일하지 않나?
기존 로직은 다음과 같습니다.
- 로그아웃 시 redis 에 access token을 저장(블랙리스트)하고 다른 요청이 들어올 경우 이 redis에 저장된 토큰인지 확인
만약 redis에 저장된 토큰이라면 예외처리
그런데 서버 부하를 줄이기 위해 JWT를 사용했는데, JWT의 단점인 탈취 시의 취약점을 상쇄하고자 Redis를 사용한다면, 기존 세션과 다를 바가 있는가? 라는 의문이 들었습니다.
이렇게 블랙리스트를 도입한다면 가볍다는 JWT의 장점도 제대로 활용 못하고 세션을 사용하는 것보다 보안이 떨어지는 것 같다고 생각했습니다.
따라서 블랙리스트를 삭제하기로 결정했습니다. Access Token은 1시간의 짧은 유효기간을 가지고 있고 이를 재발급하기 위한 Refresh Token은 httpOnly 쿠키로 보호되어 있기 때문에, Access Token을 자주 재발급해도 Refresh Token을 탈취당할 위험도 적으면서 Access Token을 탈취당해도 시간상 한계가 있어 공격 당할 위험이 적다고 판단했기 때문입니다.
⭐ 느낀 점
서버 안전성과 사용자 경험은 Trade-Off 관계를 가집니다. 개발/운영 중인 서비스의 주어진 환경에 맞춰 개발자의 적절한 선택이 필요함을 깨달았습니다. 최근 많은 기업이 MSA 방식으로 서비스를 만들기 때문에 확장성이 좋은 JWT 방식을 도입했지만 정작 저희 서비스는 단일 서버를 사용하기 때문에 JWT의 장점을 온전히 느끼지 못한 것 같습니다. 이후 프로젝트에서는 두 개 이상의 서버를 사용해봐야 겠다는 생각이 들었습니다.
참고
인증 방식 : Cookie & Session vs JWT
Redis를 이용한 토큰 탈취 대응 시나리오(feat. Refresh Token Rotation)
Access Token의 문제점과 Refresh Token
Refresh Token Rotation 과 Redis로 토큰 탈취 시나리오 대응
'project' 카테고리의 다른 글
[JazzMeet] 도메인 간 쿠키 공유 되지 않는 문제 해결 (0) | 2024.05.12 |
---|