[밍글] Spring Security + JWT 로직 개선 사례

KIM TAEHYUN·2023년 5월 31일
0

Spring Security

목록 보기
1/3

💡 4번에 걸친 토큰 재발급 로직 수정을 통한 성능/보안 향상

유학생들을 위한 익명 커뮤니티 앱 "밍글"을 개발하면서 실제 서비스될 앱이기에 제대로 보안에 신경 쓰며 인증/인가를 구축하고 싶었습니다.

이에 Spring Security를 접했고 Spring Security 없이 단일 JWT를 사용했던 인증 방식부터 Spring Security와 Redis, Access Token + Refresh Token을 도입하며 총 4번의 수정을 거친 과정을 적어보고자 합니다.

최종적으로 구현한 로직은 아래와 같습니다.

클라이언트 ↔ 서버 최종 API 요청 & 토큰 재발급 로직

  • 유저가 로그인 성공 시 Access Token, Refresh Token 동시 발급.
  • 클라이언트(앱)에서 둘 다 저장 후 API요청 시 Access Token 을 헤더에 넣어 요청
  • accessToken이 살아있다면 호출한 API 응답, 만료됐다면 accessToken이 만료됐다는 응답을 리턴.
  • 만료됐다는 응답을 받을 시 클라이언트는 accessToken 재발급 API 호출
    • parameter로 Refresh Token 과 암호화된 이메일(유저 식별용) 필요
    • Redis 에서 이메일(key)로 Refresh Token을 찾아 비교 후 새로 발급된 Refresh Token으로 대체
  • 클라이언트는 새로 발급받은 Access, Refresh Token을 저장 후 새 access Token으로
    이전의 API 재호출

1차 모델

Single JWT

  • 하나의 인증 JWT를 유저에게 발급해줌
  • 스프링 시큐리티 미적용
  • Advantage
    • 로직을 설계하기 쉬움
  • Disadvantages
    • 토큰이 탈취될 경우 유저 계정 해킹의 위험이 높음
    • 신고된 유저를 강제로 로그아웃시킬 수 없음

로그인 시 토큰에 userId를 담아 발급해주었습니다. 클라이언트는 발급 받은 JWT를 헤더에 담아 요청을 하면 서버에서 토큰을 파싱해 userId를 추출 후 서비스단에서 userId가 필요할 때 (게시물 수정 접근제어 처리 등) 사용했습니다.

하지만 이 방법은 header에 JWT가 들어오지 않을 경우를 고려하지 않았고, 토큰이 없는 유저의 API 요청을 막는 등의 인증 설정을 할 수 없었습니다. 재학생만 쓸 수 있는 앱이었기에 로그인을 한 유저 (토큰이 있는 유저)에게만 접근 권한을 주어야 했습니다. → 시큐리티의 필요성

또한, 토큰 재발급 로직이 없었기에 유저의 잦은 재로그인을 방지하기 위해 JWT의 만료기간을 길게 두었었고 (3개월), 유저가 신고를 당해 커뮤니티 이용을 정지당하더라도 이미 발급된 토큰으로 보내는 요청을 막을 수 없었습니다.

2차 모델

Spring Security + Access Token & Refresh Token + 재발급

  • 로그인 시 Response로 Access Token과 Refresh Token을 동시에 유저에게 발급해줌
  • Access Token은 짧은 만료 기한을 가지고 있고 Refresh Token은 상대적으로 긴 만료 기한을 가지고 있음
  • Access Token이 만료되었을 시 Refresh Token을 통해 Access Token을 재발급받을 수 있음
  • Advantage
    • Single JWT 방식에 비해 상대적으로 안전함
    • Access Token이 탈취당했을 경우에도 짧은 만료기한으로 보안 위험성 저하
  • Disadvantages
    • Refresh Token과 Access Token이 동시에 탈취되었을 때 해킹의 위험성이 있음
    • Refresh Token을 발급하고 인증할 때 유저 정보를 Database에서 직접 찾아야 함 → 네트워크 사용량 증가 및 응답 속도 저하
    • 클라이언트와 서버 통신 횟수 증가 (API 호출 → 토큰 만료 응답 → 재발급 API 호출 → 새 토큰 응답 → 새 토큰으로 다시 API 호출)

이에 Spring Security를 도입하면서 로그인 후 Access Token을 발급받은 유저만 헤더에 토큰을 담아 요청을 보내 인증이 되어 API를 호출할 수 있게 되었습니다.

또한, 토큰 재발급 API를 만들어 Access Token의 만료시간을 30분으로 짧게 설정하고 Refresh Token의 만료기간을 6개월로 해 유저의 잦은 재 로그인을 방지했습니다. 6개월 동안 유저의 활동 (API 요청)이 없었다면 refresh token이 만료되어 access token 재발급이 불가능해 재 로그인이 필요할 것입니다.


하지만 한 API 요청을 보낼 때 Access Token이 만료되었다면 재발급 절차를 거쳐야 하기에, 클라이언트와 서버의 통신 횟수가 늘어나고, 이로 인한 API 응답이 살짝 느려질 수 있다는 단점이 있었습니다.

그럼에도 불구하고 이 방식을 택한 이유는 다음과 같습니다.

  • 마지막 Access Token 발급 시간으로부터 30분이 지나고 나서 API 요청을 보낼 때만 재발급 로직을 거치고, 재발급 후 새로운 Access Token으로 30분 동안은 재발급 로직을 거치지 않기에 그 뒤의 API 응답 속도에 영향을 주지 않습니다.
  • 즉, 유저가 앱을 들어올 때 처음으로 호출되는 API만 재발급 로직이 수행될 것이며, 이는 홈 화면의 배너 API로 사용성에 크게 저하를 주지 않을 것이라 판단했습니다.
  • 또한, 익명 커뮤니티이기에 보안에 최대한 신경 쓰고자 했습니다.

3차 모델

Access Token & Refresh Token with Redis DB

  • 로그인 API 요청시발급된 Refresh Token을 Redis DB에 저장
    • key: email, value: refreshToken으로 저장
  • 재발급 API 요청 시 Remote Database(AWS RDS)에서 유저 정보를 가져오는 대신 Redis에서 가져옴
  • Redis에서 신고된 유저의 Refresh Token을 삭제함으로써 강제 로그아웃 구현 가능
  • Advantages
    • 낮은 네트워크 사용량, 짧은 응답속도
    • 신고된 유저 강제 로그아웃 가능
  • Disadvantages
    • Refresh Token과 Access Token이 동시에 탈취되었을 때 해킹의 위험성이 있음 (Refresh Token의 만료기간까지 재발급이 가능)

Refresh Token을 RDB에 저장하는 게 아닌 Redis에 저장하며 저장된 토큰을 가져올 때 더 빠르게 응답을 할 수 있었습니다.
또한, Redis에 Refresh Token을 저장할 때 expiration timeout for key를 Refresh Token 만료기간과 동일하게 설정함으로써 만료시간이 되면 자동으로 Redis에서 삭제되게 하였습니다.

RDB에 저장을 하게 된다면 재발급을 할 때
1. 클라이언트에게 받은 Refresh Token과 동일한 Refresh Token이 RDB에 존재하는지 확인
2. Refresh Token을 parse해 만료되지 않았는지 확인

두가지 과정을 거쳐야 했는데, Redis를 도입하며
1. 클라이언트에게 받은 Refresh Token과 동일한 Refresh Token이 Redis에 존재하는지 확인,
존재하지 않는다면 만료되어서 삭제된 것으로 토큰 검증 로직을 간소화할 수 있었습니다.

또한, 이후에 ec2에서 ECS Fargate로 전환하며 ec2에 redis를 설치해 쓰다가 AWS ElastiCache(Redis)로 전환하는 과정을 거쳤습니다.

4차 모델 (최종)

Access Token & Refresh Token with Redis DB, Reissue Refresh Token

  • Access Token이 재발급 될 때 마다 Refresh Token도 재발급해준다.
  • Access Token과 Refresh Token이 동시에 탈취되었더라도 Access Token 만료 후 재발급 시 두 개의 토큰 모두 재발급이 됨 → Redis의 timeout기능 덕분에 이전의 탈취된 토큰은 자동 폐기됨
  • Advantages
    • 낮은 네트워크 사용량
    • 짧은 응답속도
    • 신고된 유저 강제 로그아웃 가능
    • Refresh Token과 Access Token이 동시에 탈취되었을 때 상대적으로 안전

3차 모델에서 언급된 Refresh Token과 Access Token이 동시에 탈취되었을 때 해킹의 위험성(Refresh Token의 만료기간까지 재발급이 가능) 문제를 해결하기 위해 Access Token이 재발급 될 때마다 Refresh Token도 재발급 하는 방법을 택했습니다.

Redis에 토큰을 저장할 때 {email, Refresh Token} 으로 unique 한 email을 key로 사용하기에, Access Token이 만료되어 재발급을 할 때마다 Redis에 저장된 Refresh Token이 대체됩니다.

이에 이전에 발급했던 Refresh Token은 효력이 없어지게 됩니다.


이렇게 총 4번의 수정을 걸쳐 API 요청 및 토큰 재발급 로직을 개선해보았습니다.

다음 글은 실제로 코드와 함께 Spring Security 적용기를 작성해보겠습니다.

감사합니다.

0개의 댓글