GPT 요약
My-Books 프로젝트에서 초기 JWT 방식의 보안 취약점을 개선하기 위해 리프래시 토큰을 Redis에 저장하고 IP와 User-Agent 정보를 활용해 보안성을 강화했습니다.
토큰에는 UUID를 사용하고 Redis에 사용자 정보를 매핑해 추가적인 보안을 구현했습니다.
이를 통해 확장성과 보안성을 모두 충족하는 인증/인가 시스템을 설계했습니다.
My-Books 프로젝트에서의 경험을 다룬 글입니다.
My-Books 프로젝트에서 저의 주된 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
이번 글에는 JWT 안정성을 개선한 과정에 대해서 설명하려 합니다.
JWT의 안전성
이전 포스팅 'JWT는 왜 사용할까?'의 마지막 부분에서 다음과 같은 언급을 한 적이 있습니다.
JWT는 왜 사용할까?
GPT 요약My-Books 프로젝트에서 세션 불일치 문제를 해결하기 위해 JWT 인증을 도입했습니다.JWT는 서버 자원 없이 확장성이 높고 MSA에 적합합니다.사용 시 보안에 유의해야 합니다.My-Books 프로젝트
masiljangajji-coding.tistory.com
이번 글에는 JWT 안정성을 개선한 과정에 대해서 얘기하려 합니다.
초기의 JWT 사용
초기의 방법은 엑세스 토큰만 운용하는 방식이었습니다.
엑세스 토큰을 운용하여 토큰의 유효기간에 가까워 진다면(5~10분 이내) 엑세스 토큰을 refresh하는 요청을 주어 갱신해주는 것이죠
하지만 클라이언트가 쿠키로 내려진 토큰을 탈취당하는 경우에 이 방법은 보안이 매우 취약해집니다.
(공격자가 계속해서 토큰을 갱신하며 사용가능하기 때문에)
또한 매 요청마다 유효기간을 확인해 줘야 하기 때문에 불필요한 추가 연산이 필요해집니다.
이러한 방법을 해결하고자 다음과 같은 과정을 거치게됩니다.
JWT 안전성 개선 - 쿠키
처음에는 쿠키의 탈취를 어렵게 만드는 방법을 우선적으로 고려했습니다. 쿠키는 다양한 보안 옵션을 설정할 수 있으며, 제가 사용한 옵션은 다음과 같습니다.
- Secure - HTTPS에서만 쿠키전송
- HttpOnly - JavaScript로 쿠키 접근 불가 , XSS 방어
- SameSite - CSRF 방어를 위한 정책
이를 통해 클라이언트-서버 간 통신에서 쿠키 탈취를 어렵게 만들었습니다.
그러나 쿠키가 탈취된 이후에는 여전히 대응할 방법이 부족하다는 문제가 남았습니다.
JWT 안전성 개선 - 리프래시 토큰
여러 문서를 찾아본 결과 가장 많이 사용되는 방법이 액세스 토큰과 리프래시 토큰을 사용하는 것이었습니다.
짧은 유효기간을 가진 액세스 토큰과 긴 유효기간을 가진 리프래시 토큰을 두어 액세스 토큰 탈취 시에도 짧은 유효기간으로 인해 공격을 막을 수 있다는 것입니다.
또한 크게 2가지 운용방식이 존재했습니다.
- 액세스 토큰과 리프래시 토큰을 모두 쿠키로 내려주기
- 액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버 자원에 저장
액세스 토큰과 리프래시 토큰을 모두 쿠키로 내려주기
의 경우에 서버의 자원을 사용하지 않기 때문에 JWT의 Stateless 한 특성을 잘 살릴 수 있지만 액세스 토큰(쿠키)가 탈취당한 경우를 가정한다면 똑같이 쿠키로 저장돼있는 리프래시 토큰은 멀쩡할까?라는 의구심이 들었습니다.
액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버에 저장
의 경우에 탈취가 어려워진다는 장점이 있지만 서버에 데이터가 저장됨으로 Stateful 해진 다는 단점이 존재합니다.
저의 경우 액세스 토큰은 쿠키로 리프래시 토큰은 Redis와 같은 별도의 서버에 저장
방법을 선택했는데요
- 부분적으로 Stateful 해지는 것은 맞으나 Stateless의 이점을 전부 사용 가능함
- Redis에 리프래시 토큰을 저장하고 부르는 로직은 자주 변경 및 호출되지 않는다
- JWT 일반 문자열이기 때문에 매우 많은 사용자를 커버할 수 있다
MSA 환경이나 Scale-Out으로 구성된 서버에서 확장성을 보장하는 것이 JWT를 사용하는 가장 큰 이유라 생각합니다.
리프래시 토큰을 각각의 서버에 저장시킨다면 이는 확장성에 제약이 생길 수 있지만 목적이 아예 분리된 Redis 서버 사용 시 Front와 Back 서버에 확장에 영향을 주지 않을 것이라 생각했습니다.
또한 리프래시 토큰을 저장하고 부르는 로직은 최초 로그인 요청 혹은 액세스 토큰은 유효하지만 유효기간이 만료돼 갱신하는 경우만 존재합니다.
이는 자주 변경되거나 호출될 일이 적음을 의미하고 별도의 서버를 둠으로써 버그 발생의 위험이나 복잡도가 크게 증가한다고 생각하지 않았습니다.
마지막으로 JWT는 결국 문자열 덩어리기 때문에 큰 자원 소모 없이 많은 사용자를 커버할 수 있다고 생각했습니다.
그렇게 해서 개선된 코드는 다음과 같습니다.
Redis에 저장된 리프래시 토큰을 액세스 토큰만으로 접근이 가능하다면 문제가 생길 수 있기 때문에 IP주소를 추가로 조합해 주었습니다.
또한 액세스 토큰과 리프래시 토큰을 일회용으로 사용하게끔 만들어 보안성을 높이려 했습니다.
RemoteAddr 이슈
그런데.. 이상한 일이 발생했습니다.
왜.. Redis에 저장되는 IP 주솟값이 다 똑같지?, 심지어 내 IP 주소도 아니잖아?
로그를 남겨 IP와 관련된 모든 값을 확인해 봤습니다.
그럼에도 문제점을 파악하지 못했기 때문에 Front 서버에서 직접 헤더에 정보를 넣어 보내줬습니다.
여기서 알게된 것은 사용자의 요청이 Front 서버를 거쳐 Auth 서버로 넘어오는 것이기 때문에
실제로 확인되는 IP 정보는 Front 서버의 IP라는 것입니다.
따라서 Front 서버에서 직접 IP 주소와 User-Agent 정보를 DTO 형태로 넘겨주게 변경했습니다.
(IP 주소뿐 아니라 User-Agent 정보를 이용해 보안성을 더욱 높였습니다.)
JWT 안전성 개선 - PayLoad
그런데 코드 리뷰 중 다음의 피드백을 받게 됩니다.
지금 user_id를 그대로 토큰에 기입하고 있는데 이거 그냥 보여줘도 돼?
처음에는 사용자에게 쿠키를 내려주는 것이기 때문에 디코딩 해서 내용을 까 볼 사람이 있을까? 정도로 생각했었지만 더 깊게 생각해 보니 이 정보를 기반으로 사용자의 정보나 API의 형태를 유추 가능해짐을 알았습니다.
또한 user_id 정보를 굳이 알려줄 이유도 없기 때문에 토큰에는 UUID를 기입하고 Redis에 user_id정보를 넣어줬습니다.
Key : UUID+IP+User-Agent , Value : user_id
최종적으로 구성된 코드입니다.
UUID를 기반으로 새로운 키를 생성해 Redis에 user_id를 기입하는 코드가 추가됐습니다.
또한 인증/인가 프로세스의 추가적인 개선 가능성을 모색했습니다. 정보를 추가적으로 저장하는 것에 비해 얻는 보안상에 이점을 더 갖고싶었기 때문입니다.
그 과정에서 기존 로그아웃 프로세스를 점검하던 중 로그아웃 이후에도 기존 액세스 토큰이 유효해 재사용이 가능하다는 문제또한 발견할 수 있었습니다.
JWT 안전성 개선 - UUID를 활용한 추가 보안 로직
기존의 GatewayFilter입니다.
토큰의 유효성과 사용자의 권한 및 상태를 확인하는 절차를 가집니다.
로그아웃시 사용자의 쿠키에 있는 정보는 지워지지만 그 정보를 기억하고 있다면 토큰 자체는 유효하기 때문에 Filter를 통과해 요청을 보낼 수 있게됩니다.
기존에는 이를 처리할 방법이 없었지만 추가된 UUID를 통해서 이제는 해결이 가능합니다.
- 사용자가 로그아웃하면, Redis에서 해당 UUID에 매핑된 정보를 즉시 삭제
- Gateway에서 Redis 상태를 조회하도록 로직 추가
- 이로 인해 이후의 액세스 토큰 검증 과정에서 UUID를 찾을 수 없게 되어 토큰을 Invalid로 판단
- Redis와 UUID를 활용한 상태 검증으로 로그아웃 이후 액세스 토큰 재사용을 완벽히 차단
인증서버에 로그아웃시 UUID정보를 삭제하는 로직 추가 후
Gateway에도 UUID를 기반으로 유저를 찾는 로직을 추가해줬습니다.
그 후 Filter의 리펙토링 과정을 거치게되면...
redisService의 isValidateUser를 이용해 액세스 토큰 재사용을 막은 후 토큰과 유저의 권한 및 상태를 확인하게 됩니다.
이 Filter를 기반으로 최종적으로는 다음과 같은 프로세스가 완성됩니다.
이렇게 단계별로 JWT의 안전성을 개선한 여정은 마무리됩니다.
같이 보시면 좋은 발표 영상입니다.
'프로젝트 > My-Books' 카테고리의 다른 글
BCrypt로 완성한 안전한 로그인 설계 (1) | 2025.01.05 |
---|---|
Gateway로 인가 로직, 어떻게 최적화했을까? (0) | 2024.12.22 |
JWT 만료기간, 보안과 사용자 경험의 Trade-Off (3) | 2024.12.21 |
Spring Security 없이 효율적인 인가 구현하기 (1) | 2024.12.20 |
JWT는 왜 사용할까? (1) | 2024.12.20 |