JWT 보안성 강화시키기 (Access Token, Refresh Token -RTR, BlackList 그리고 Redis)

2024. 10. 2. 20:56·[TIL]

📌개요

프로젝트를 진행하면서 로그인 유지 기법  으로는 크게 세션 VS 토큰으로 비교 대상군이 되고는 합니다.

이 중에서 저는 세션 방식과 토큰 방식 중 왜 토큰 방식을 택했으며, 

토큰 방식 중 JWT를 선택하게 되면서 얻었던 보안적인 고찰에 대해서 적어보고자 합니다.

 

해당 포스팅은 JWT에 대한 설명 보다는 JWT 에 보안을 올려왔던 과정에 대한 글 인점을 참고해주세요!

 

 

🔍프로젝트에서 토큰 방식을 택했던 이유

저는 서버를 단일 서버가 아닌 쿠버네티스 환경에 올렸기 때문에,

쿠버네티스 환경을 통해서 서버의 개수를 자유롭게 늘리고 줄일 수 있는 자율성을 가질 수 있었습니다. 

 

하지만 세션 방식의 가장 큰 단점이라고 한다면 아래와 같을 것 입니다.

세션 정보의 가장 큰 단점은

회원이 서버1 에서 로그인 요청을 하고 세션 정보를 공유하게 되었지만 만일 로드밸런서에 의해서 서버2 에게 다음 요청 작업을 부탁하게 되었다면 서버2는 동기화 과정 없이는 세션 정보를 알 수가 없어서 다시 로그인을 해야하는 상황이 발생하게 됩니다.

 

이에 따라 서버가 세션 정보를 직접 관리하여 로그인을 유지시키는 방법 보다는
클라이언트 측의 토큰에 따라 검증 과정만 거쳐 유지 시키는 것이 나을 것 같다 라는 판단을 하게 되어 아래와 같이 토큰 방식을 채택하였습니다.

 

토큰의 장점이라고 한다면, 세션과 달리 서버간의 동기화 과정이나, 고정된 서버로 보내는 추가 작업을 해주지 않고, 직접 전달받은 토큰에 의해서만 판단하면 될 것 입니다.

 

하지만 서버가 정보를 관리하지 않는다. 라는 것은 편할 수도 있겠지만, 양날의 검 처럼 굉장히 치명적인 단점을 가지고 있습니다.

문제는 토큰이 탈취 될 경우에 발생합니다.

 

🥊 서버는 정상적인 유저의 토큰인지, 해커에게 탈취된 토큰인지 알 수가 없다.

 

실제로 토큰 방식에서 가장 큰 문제점으로 삼는 것은 바로 "보안성"일 것 입니다.

JWT 특성상 암호화를 건다고는 하지만, 서버의 입장문으로서 작용되는 토큰은 결코 정상 토큰인지, 탈취된 토큰인지 알 수 없습니다. 

따라서 반드시 해당 상황에 대처하기 위한 추가적인 작업들이 필요 할 것 입니다.

 

 

🔨 내가 선택한 보안성 강화 방법

 

저는 아래와 같은 3가지 방법으로 보안성을 강화시켜보고자 노력하였습니다.

 

 

 

🔨 1차적 보안 : Http Only, Secure 설정

쿠키로 전달하는 과정 중 Http Only , Secure 의 설정을 추가적으로 걸어줌으로서, 일련의 보안성을 높일 수 있었습니다.

 

해당 옵션들의 효과는 아주 간단히 설명드리자면 다음과 같습니다.

 

1. Http Only : 해당 옵션은 클라이언트단에서 javascript 로 쿠키를 조작할 수 없습니다. 따라서 불법적인 접근을 조금이나마 막을 수 있을 것 입니다.

2. Secure : 해당 옵션은 HTTP 가 아닌 HTTPS 에서의 환경에서만 쿠키를 주고받을 수 있게 설정합니다. 

즉, HTTP 환경에서는 쿠키가 절대로 세팅 될 수 없기에 SSL 이라는 보안의 힘을 업어갈 수 있을 것 입니다.

 

 

하지만, 만일 해커가 1차적인 보안을 뚫고 Access Token 을 탈취 할 경우에는 어떨까요?

1차적인 보안만을 가지고 보안성을 가지기에는 다소 위험성이 높습니다.

 

따라서 저는, 2차적인 보안을 위해 Refresh Token 전략을 도입하게 됩니다.

 

 

🔨 2차적 보안 : Refresh Token 추가 전달

 

저는 만일 1차적인 보안을 뚫고 탈취가 되었을 때에도 대처하기 위해서 2차적인 보안 방법으로 Refresh Token 을 함께 보내는 전략을 채택하였습니다. 

Refresh Token에 의해서 재발급이 가능하기에, AccessToken의 만료시간을 아주 짧게 설정할 수 있었고,

아래와 같이 Access Token 이 탈취되더라도 오직 30분이라는 시간만 주어지기에 탈취 되어도 위험성을 낮출 수 있었습니다.

 

 

하지만, Refresh Token을 구현하신 분들 중, 혹시라도 이런 생각을 해보신 적이 있으신가요?

 

엥? 만약에 Access Token 과 Refresh Token이 둘 다 탈취되면 큰일 나는거 아니야?

 

 

❗❗Refresh Token을 전달 할 경우에는 반드시 추가적인 작업이 필요하다!

 

저도 위와 같은 고민을 하게 되었고, 실제로, Refresh Token만 구현하고 아무런 대처를 하지 않는다면, 그저 해커가 더욱 신나게 날 뛸 수 있도록 도와주는 상황이 발생할 수도 있습니다.

 

따라서 저는 Refresh Token에 대해서도 보안을 강화해주기로 결심하게 됩니다.

 

 

 

💪 Refresh Token 보안 전략 2가지 ( RTR / BlackList ) 

 

👌첫 번째 전략 : Refresh Token Rotation 이라고 칭하는 RTR 전략

해당 전략을 간략히 설명드리자면, 기존에는 AT(Access Token)가 만료시 AT 만 재갱신을 시켜주었었다면, 이제는 RT(Refresh TOken) 또한 재갱신을 시키는 방법입니다! 

 

해당 방법의 효과는 다음과 같습니다.

RT 가 재갱신이 될 때 해당 토큰을 DB에 저장을 하고, 이전에 가지고 있던 RT 는 쓸모가 없어지는 것 입니다. 즉, 해커가 가지고 있는 RT 로는 이제 더이상 AT의 재갱신이 불가능합니다. ( 1회용성 RT 라고도 불리웁니다. ) 

if(jwtUtil.isExpired(accessToken)){

    String reIssuedAccessToken = refreshTokenService.reIssueAccessToken(refreshToken);


    ---검증 과정 중략---


    // AccessToken 재갱신
    token = reIssuedAccessToken;
    Cookie cookie = new Cookie("JToken", token);
    cookie.setHttpOnly(true);
    cookie.setSecure(true);
    cookie.setPath("/");
    response.addCookie(cookie);


    // RefreshToken 재갱신
    refreshToken = refreshTokenService.reIssueRefreshToken(refreshToken); //RTR 적용
    Cookie reissuedRefreshToken = new Cookie("RefreshToken", refreshToken);
    reissuedRefreshToken.setHttpOnly(true);
    reissuedRefreshToken.setSecure(true);
    reissuedRefreshToken.setPath("/");
    response.addCookie(reissuedRefreshToken);
}

 

하지만 해당 RTR 전략도 분명 허점이 존재합니다. 

만일, 사용자가 가장 마지막에 사용한 RT 를 해커가 확보하게 된다면 어떻게 될까요?

 

가장 마지막에 사용 된 RT 를 해커가 습득한다면, 유저가 다시 로그인이나 만료를 맛보지 않는이상, 해당 무한 갱신의 역할은 해커가 가지게 됩니다. 

 

따라서 저는 마지막 전략을 세우게 되었습니다.

 

👌 두 번째 전략 : 로그아웃 BlackList 전략

BlackList 전략은 로그아웃시에, Database 에 아예 블랙리스트로 등록해버리는 전략입니다.

해당 효과는 다음과 같습니다.

유저는 본인의 웹사이트 방문이 모두 끝나면, 로그아웃 요청을 보내게 됩니다. 로그아웃 요청이 들어오게 되면,

AT 와 RT 모두 Database 에 블랙리스트 토큰을 저장시킵니다. 

 

이말은 즉슨, 이제 해당 토큰은 블랙리스트 이기 때문에, 이후에 들어오는 같은 토큰은 모두 탈취된 토큰으로 간주하겠다. 를 뜻합니다. 이후에 토큰이 들어오게 되면 해당 클라이언트가 가지고 있는 쿠키를 그 즉시 삭제시킬 수 있게됩니다.

 

 

JWT 확인 필터

// 1. 쿠키를 통한 요청 받기
if(request.getCookies() != null){
    for(Cookie cookie : request.getCookies()){
        if(cookie.getName().equals("JToken")){
            accessToken = cookie.getValue();
        }
        if (cookie.getName().equals("RefreshToken")) {
            refreshToken = cookie.getValue();
        }
    }
}


// 2. 블랙리스트에 등록되어 있으면 불법 토큰으로 판단
    if(blackListTokenService.checkBlackList(accessToken,refreshToken)){
        System.out.println("블랙리스트 토큰");

        // 불법 쿠키 삭제
        Cookie deleteAccessTokenCookie = new Cookie("JToken", null);
        deleteAccessTokenCookie.setMaxAge(0);
        deleteAccessTokenCookie.setPath("/");
        response.addCookie(deleteAccessTokenCookie);

        Cookie deleteRefreshTokenCookie = new Cookie("RefreshToken", null);
        deleteRefreshTokenCookie.setMaxAge(0);
        deleteRefreshTokenCookie.setPath("/");
        response.addCookie(deleteRefreshTokenCookie);

       ---중략---
    }

 

BlackList 탐색 서비스

public class BlackListTokenService {

    private final BlackListTokenRepository blackListTokenRepository;

    public boolean checkBlackList(String accessToken, String refreshToken){
        boolean existAccess = false;
        boolean existRefresh = false;
        if(accessToken != null){
            existAccess = blackListTokenRepository.existsByToken(accessToken);
        }
        if(refreshToken != null){
            existRefresh = blackListTokenRepository.existsByToken(refreshToken);
        }

        if(existAccess || existRefresh){
            return true;
        }
        return false;
    }
}

 

이로써 다양한 보안과정을 통해서 Token에 보안성을 강화 시켜줄 수 있었습니다.

 

하지만 하면 할수록 이럴꺼면 세션방식을 ㅆ

 

 

😂 RDB 에 토큰을 저장시켰을 때의 문제점 발생

그러나 블랙리스트의 가장 큰 단점은 로그아웃한 모든 토큰을 db에 저장시킨다는 점입니다.
보안을 높이기 위해서, 토큰의 정보를 DB가 관리하게 되면서, DB에 대해서 부하가 굉장히 늘어나게 되는 것을 본 프로젝트를 진행하며 알게 되었습니다.

 

짧은 AT의 만료시간 마다, blackList 조회는 일어나고,
매 로그아웃 요청마다 DB안에 2개의 데이터도 고정적으로 쌓이게 되어 주기적으로 삭제 시켜주는 작업까지 필요하게 되었습니다.

 

 

서비스를 이용하는 유저 중 로그아웃 횟수가 571번 일 때만 하더라도 DB 안에 순식간에 1142개의 토큰이 삭제되지 않고 계속 저장되는 것을 볼 수 있습니다.
만약 유저가 1만명, 10만명 이라면 로그아웃 요청 횟수마다 DB에 저장되는 토큰은 상상 이상일 것입니다.

 

이에 따라 Redis 도입을 결정하였습니다.

 

 

👊 최종, Redis 도입

Redis는 In-memory 방식이므로 디스크가 아닌 메모리에 저장되기 때문에 데이터를 조회할때 속도가 빠르고,
TTL 기능을 기본적으로 제공 해주기 때문에, 만료된 토큰을 자동 삭제하여 DB의 부하를 줄일 수 있어 성능 개선에 적합하다고 판단하였습니다.

 

RDB에 있는 정보를 Redis로 옮기게 되면서 성능적인 개선은 다음과 같습니다.

 

  • DB에서 조회 하지않고, 사전에 In-memory Database 에서 값을 조회하기에, 조회 속도가 빠르며 DB에 대한 부담도 줄일 수 있었다.
  • TTL 기간을 RefreshToken 의 만료시간과 동일하게 설정해주어, 개별 토큰마다 자동삭제가 가능해졌다.

 

 

마무리

물론 아직까지, 회원이 사이트 이용을 마치고, 로그아웃 요청을 진행하지 않으면 대처하기는 다소 어려움이 있다라는 숙제는 남아있으나, 이러한 일련의 과정을 통해서 본 서비스에서 토큰에 대한 보안성을 높일 수 있었습니다. 💪🙇‍♂️

 

 

이상 해당 포스팅을 읽어주셔서 감사합니다 😀

'[TIL]' 카테고리의 다른 글

[ TIL ] RECOVER_YOUR_DATA : RDS 해킹 일지  (1) 2024.11.13
Kubernetes 분산 서버에 의한 Oauth2 소셜 인증 실패 및 극복 방법 (sticky session service 추가)  (2) 2024.10.13
ELK 에 대해서 알아보자! ( ElasticSearch - Logstash - Kibana )  (2) 2024.09.22
Cookie 에 Http Only 와 secure 를 적용시켜보자! [ Spring boot + Vue 3.x]  (0) 2024.08.26
[TIL] 도메인 적용 방법 + HTTP에서 HTTPS로 전환 방법 [Nginx, 내도메인한국, Zerossl]  (1) 2024.08.16
'[TIL]' 카테고리의 다른 글
  • [ TIL ] RECOVER_YOUR_DATA : RDS 해킹 일지
  • Kubernetes 분산 서버에 의한 Oauth2 소셜 인증 실패 및 극복 방법 (sticky session service 추가)
  • ELK 에 대해서 알아보자! ( ElasticSearch - Logstash - Kibana )
  • Cookie 에 Http Only 와 secure 를 적용시켜보자! [ Spring boot + Vue 3.x]
7.06com
7.06com
우당탕탕 코딩하기
  • 7.06com
    우당탕탕 개발자의 이야기
    7.06com
  • 전체
    오늘
    어제
    • 분류 전체보기 (63)
      • [Spring] (7)
      • [JAVA] (3)
      • [디자인패턴] (1)
      • [TIL] (8)
      • [CI,CD] (5)
      • [협업] (1)
      • [Database] (5)
      • [CS] (3)
      • [코딩테스트] (15)
      • [알고리즘] (0)
      • [후기-회고] (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
7.06com
JWT 보안성 강화시키기 (Access Token, Refresh Token -RTR, BlackList 그리고 Redis)
상단으로

티스토리툴바