JWT(JSON Web Token)

하쮸·2025년 1월 14일
0

쿠키, 세션, 토큰

목록 보기
2/2

1. JWT(JSON Web Token).

  • JSON 웹 토큰(JWT)은 온라인 네트워크에서 정보를 안전하게 통신할 때 사용하는 인터넷 표준 토큰임.
    • 인증, 정보 교환 등 다양한 용도에 사용됨.
    • 이때 주고받는 정보를 클레임(Claim)이라고 하고, 클레임의 집합은 JSON 객체로 표현함.
  • JWTRFC 7519 웹 표준으로 지정되어 있음.
  • JSON(JavaScript Object Notation) 객체를 사용해서 토큰에 정보들을 저장하고 있는 웹 토큰(Web Token)이라 정의할 수 있음.

(참고) rfc7519

JWT 공식 사이트.

JWT 구성.
JWT 구성

  • Header.

    • alg에서는 서명에 사용된 알고리즘을 나타냄.
    • typ에서는 토큰 타입 지정.
    • JSON으로 표현된 헤더를 Base64로 인코딩한 것이 JWT 헤더.
  • Payload.

    • 서버와 클라이언트가 주고 받는, 시스템에서 사용될 정보가 저장.
      • JWS 방식에서는 페이로드도 Base64로 인코딩하기 때문에 누구나 디코딩할 수 있어서 JWS 페이로드에는 민감한 정보를 넣으면 안됨.
      • JWE 방식에서는 페이로드를 안전한 알고리즘과 비밀 키로 암호화하기 때문에 민감한 정보를 포함할 수 있음.
    • 보통 JSON 형식으로 표현된 사용자의 정보나 클레임이 키-값(key-value)로 포함된 부분.
      • Claim이라는 용어는 Payload에 들어 있는 데이터 항목(Key-Value 쌍)을 뜻함.
        Key 자체라기보다는 Key와 Value를 포함한 전체 정보를 의미함.
    • 클레임(claim)은 크게 3가지로 분류.
      • 등록된 클레임(Registered Claims)
        • iss : JWT의 발급자(issuer).
        • sub : JWT의 제목(Subject)
        • aud : JWT의 수신인(Audience)
        • exp : JWT 만료시간(Expiration)
        • nbf : Not Before 의미로, 토큰이 유효해지기 시작하는 시간.
        • iat : JWT가 발급된 시간(issued at)
        • jti : JWT의 식별자(JWT ID)
      • 공개 클레임(Public Claims)
        • 공캐 클레임은 키 값을 마음대로 정의할 수 있음.
          (충돌이 안되는 범위 내에서)
      • 비공개 클레임(Private Claims)
        • 통신 간에 상호 합의되고 등록된 클레임과 공개 클레임이 아닌 클레임을 의미함.
  • Signature.

    • Base64로 인코딩된 헤더와 페이로드를 결합한 후 alg에 명시된 알고리즘과 비밀 키 또는 공개 키로 서명한 값.

      • 이 서명은 JWT의 무결성을 보장하고 데이터가 변경되지 않았음을 확인할 수 있음.
      • 서명은 키로만 복호화할 수 있기 때문에 토큰의 전송자와 내용의 무결성을 보장함.
    •   Signature = [Header_alg에서 지정한 알고리즘](
          "Base64UrlEncodedHeader + "." + Base64UrlEncodedPayload,
          secretKey
      )
      
    • 토큰의 유효성 검증을 위한 문자열.

      • Signature는 토큰의 값들을 포함해서 암호화하기 때문에 이걸 통해서 서버는 해당 토큰이 유효한 토큰인지 검증할 수 있음.

간단한 JWT 로직.


1-1. JWT의 유형.

  • JWS(JSON Web Signature)
    • JWS 방식을 사용하면 클레임의 내용은 누구나 읽을 수 있지만 서명이 있기 때문에 데이터의 무결성이 보장됨.
  • JWE(JSON Web Encryption)
    • JWE에서는 클레임 자체를 암호화 해서 복호화 방법을 알고 있는 사용자만 페이로드를 읽을 수 있음.
    • 보안 측면에서는 JWE가 더 안전함.
  • JWS, JWE 둘 다 구성은 같음.
  • 페이로드의 데이터를 클라이언트가 바로 사용해야 된다면 JWS가 더 편리하고 데이터의 무결성을 보장할 수 있음.
    • 용도의 보안, 사용성을 고려해서 JWT 방식을 선택하는 게 좋음.

1-2. JWT, SAML 비교.

-JWTSAML
형식- JSON (Base64Url 인코딩)XML (Base64 인코딩)
사용 사례- API 인증, 마이크로서비스 통신, 모바일 및 웹 애플리케이션.- 싱글 사인온(SSO), 엔터프라이즈 애플리케이션.
전송 방식- HTTP 헤더 (주로 Authorization: Bearer로 전달)- HTTP POST, 리디렉션 또는 SOAP 메시지
크기- JSON 기반이라 더 작고 경량화됨.- XML 기반이라 크기가 더 크고 무거움.
서명 방식- alg 필드에 명시된 알고리즘 사용 (예: HMAC, RSA 등)- XML 디지털 서명 사용.
사용자 정보(인증 관련 데이터)- Claims(클레임)을 사용하여 사용자 정보 포함.- Assertion을 사용하여 인증 및 권한 부여 정보 포함.
유연성- 간단하고 RESTful API와 쉽게 통합 가능.- 복잡하며 주로 엔터프라이즈 환경에서 활용.

1-3. 인증, 요청 필터의 로직.


1-3-1. 인증 필터.

  • JwtAuthenticationFilter

    • UsernamePasswordAuthenticationFilter 필터를 상속한 커스텀 인증 필터.
    • 클라이언트에서 제공한 아이디 비밀번호로 로그인 인증 처리를 해주기 위한 필터.
  • UsernamePasswordAuthenticationFilter

    • Spring Security에서 제공하는 폼 로그인 기반으로 인증 처리를 해주기 위한 필터.
  • 인증 처리 프로세스

    • 지정한 로그인 경로 (/login) 에 요청 시, 필터를 적용.
    • 인증 시도
      • 요청 메시지에서 아이디, 비밀번호를 추출.
      • 아이디, 비밀번호로 UsernamePasswordAuthenticationToken 객체를 생성.
      • AuthenticationManager를 통해서 UsernamePasswordAuthenticationToken으로 인증을 시도함.
      • 스프링 시큐리티에서 인증 처리를 함.
        • UserDetailsService 에서, 사용자 정보인 UserDetails 객체를 조회.
        • UserDetails객체에 저장된 비밀번호를 확인하기 위해서 PasswordEncoder를 사용.
        • 사용자가 입력한 비밀번호랑 저장된 비밀번호가 일치하는 확인.
        • 인증에 성공하면, Authentication 객체를 생성하여 반환.
        • Authentication의 isAuthenticated() 메소드로 인증 성공 여부를 확인할 수 있음.
          • 인증 성공 시, JWT 토큰 발행.
          • Authentication의 Principal객체 안에 사용자 인증 정보를 가져옴.
          • Principal 객체의 아이디, 권한 목록을 추출.
            - JWT 토큰을 생성.
          • Authorizaion 응답헤더에 “Bearer ” + {JWT} 형태로 JWT를 담음.
            - JWT를 클라이언트에 응답.

1-3-2. 요청 필터.

  • JwtRequestFilter
    • 클라이언트에서 보낸 요청에 담긴 JWT 토큰을 검증하는 필터.
    • 클라이언트의 요청마다 JWT 토큰을 해석해서 인증 처리하기 위해서 사용.
  • 요청 처리 프로세스.
    • 클라이언트는 최초 로그인 인증을 한 뒤, 요청을 할 때 마다 Authorization 요청 헤더에 jwt 토큰을 포함하여 요청함.
      • 사전에 먼저 클라이언트는 아이디 비밀번호로 인증 요청을 통해 JWT를 응답받음.
      • JWT를 쿠키 등의 클라이언트 저장소에 저장.
      • 최초 인증 후, 매 요청마다 Authorization 요청 헤더에 담아서 요청을 보냅니다.
    • 매 요청의 인증 처리를 요청 필터를 통해서 처리함.
    • Authorization 요청 헤더에서 JWT 추출합니다.
      • JWT 토큰 해석하여 인증 시도를 함.
      • JWT 토큰을 검증.
      • JWT 토큰이 유효하고, 인증이 성공되면 인증 완료 처리.

2. JWT 장단점.

  • 장점.
    • 간결하고 URL에 안전하게 사용할 수 있음.
    • 데이터 저장소에 대한 의존성이 없음.
      • 시스템 수평 확장에 용이함.
    • 상태를 유지하지 않음. (Stateless)
      • 필요한 정보를 모두 자체적으로 들고 있어서 서버 측에서 세션을 저장할 필요가 없기 때문에 서버 부하를 줄일 수 있고 확장성을 확보할 수 있음.
    • JWT를 사용하면 무결성이 보장됨.
      • JWS는 서명을 통해 토큰이 변조되지 않았음을 확인할 수 있고
        JWE는 페이로드를 암호화하기 때문에 데이터의 기밀성까지 보장할 수 있음.
    • BASE64 URL Safe Encoding을 이용하기 때문에 URL, 쿠키, 헤더 어디에서든 사용할 수 있는 범용성을 가지고 있음.
  • 단점.
    • Payload에 저장하는 정보가 많아지면 트래픽의 크기가 커짐으로써 네트워크 사용량이 증가할 수 있음.
    • 토큰이 서버에 저장되지 않고 각 클라이언트에 저장되기 때문에 서버에서는 각 클라이언트에 저장된 토큰 정보를 직접 조작할 수 없음.
    • Signature(서명)을 제외한 부분은 누구나 들여다 볼 수 있음.

3. accessToken, refreshToken.

  • JWT 또한 제 3자에게 탈취 당할 위험이 있기 때문에 도입한 것이 바로 accessToken과 refreshToken임.
    • accessToken
      • 유저의 정보가 담긴 토큰.
        • 탈취 당할 위험이 존재하기 때문에 만료 시간이 비교적 짧음.
      • 클라이언트에서 요청을 보낼 때 같이 담아서 보내고 서버는 해당 토큰을 검증한 뒤 응답함.
    • refreshToken
      • 새로운 accessToken을 발급해 주기 위해서 사용하기 때문에 상대적으로 유효기간이 긺.

3-1. accessToken, refreshToken는 어디에 저장할까.

로컬 스토리지 or 세션 스토리지.

  • 로컬 스토리지와 세션 스토리지의 차이는 데이터의 영구성 정도.
    • 로컬 스토리지 : 브라우저를 닫아도 유지됨.
    • 세션 스토리지 : 브라우저가 닫히면 삭제됨.
  • 기존에 Access Token만 사용할 때는 브라우저의 세션 스토리지에 담을 수 있음.
    • 인증 성공 시 받은 토큰을 세션 스토리지에 저장한 뒤, 요청을 보낼 때 자바스크립트로 꺼내서 보내는 방식.
  • 로컬 스토리지나 세션 스토리지에 저장하는 방식은 XSS(Cross Site Scripting) 공격에 취약함.

쿠키.

  • 쿠키 역시 자바스크립트로 접근이 가능하므로 HTTP Only 옵션을 설정해야됨.
  • 또한 HTTPS가 적용되지 않은 리소스로 인해 쿠키를 탈취당할 수 있으므로 secure 옵션도 설정해야됨.
  • 이렇게하면 쿠키를 탈취당할 위험도 방지할 수 있고, 자바스크립트로 쿠키 값을 취득하는 것도 막을 수 있음.
    HTTP 통신 자체를 하이재킹 당하더라도, HTTPS로 암호화가 되어 있기 때문에 쿠키 값을 알아낼 수는 없음.
  • 그렇다면 쿠키는 안전한가?
    • 쿠키에 토큰을 담으면 CSRF(Cross-Site Request Forgery) 공격에 취약함.
      • XSS 공격을 통해서는 토큰 값 자체를 가져올 수 있지만, CSRF 공격을 통해서는 로그인 된 상태로 사용자가 원하지 않는 동작을 하게 만듦.
    • 즉, CSRF 공격으로 쿠키에 저장되어 있는 토큰 값 자체를 가져오는 것은 아님.
  • 만약에 어떠한 방법으로든 Refresh Token이 탈취가 될 수도 있기 때문에 이러한 위험성을 최대한 줄이기 위해 RTR(Refresh Token Rotation)이라는 방식이 있음.
    • 이 방법은 Refresh Token을 통해 Access Token을 재발급 할 때 Refresh Token을 새 것으로 교체해서 단 한번만 사용할 수 있도록 하는 방식임.
      • 여기에 이미 사용된 Refresh Token으로 요청이 들어오면 모든 Refresh Token을 폐기하는 보안 조치도 추가로 넣어준다고 함.
    • 이렇게 하더라도 사용하지 않은 Refresh Token을 탈취하면 한 번은 Access Token을 발급받을 수 있지만, 탈취된 Refresh Token이 무한정 사용되는 것은 막을 수 있음.

4. 토큰을 사용하는 이유.

  • 먼저 세션 기반의 인증 방식에 대해서 알아보자.
    • 토큰 방식이 세션 방식의 단점을 보완하고자 생긴 방식이기 때문.
    • 그러나 토큰 방식이 무조건 세션 방식보다 좋은 것은 아니고 각각 장단점이 존재함.

  • 클라이언트가 로그인 요청을 하면 로그인된 사용자들에 대한 정보들을 백엔드 내의 세션 스토리지에서 유지함.
    • 백엔드에서는 어떤 사용자들이 로그인 되어있는지 확인하기 쉽고 사용자에 대한 데이터가 서버에서 관리 되기 때문에 클라이언트쪽에서 변조가 어렵다는 장점이 존재함.
  • 세션스토리지를 백엔드에서 관리하고 있기 때문에 백엔드 서버가 무거워지게 된다는 문제가 있고 무엇보다 서버를 확장할 때 서버 한 대 내에 존재하던 세션 스토리지가 여러 대의 서버로 늘어나면서 세션 관리들을 따로 해줘야 하기 때문.

  • 위 사진처럼 로드밸런서 + 세션 클러스터링 방법을 이용해 해결할 수도 있고, 아예 redis 서버를 하나 둬서 세션을 통째로 관리하는 방법도 있음.
  • 위의 문제점들을 해결하기 위해 나온 방식이 토큰 인증 방식.

  • Access Token과 Refresh Token을 클라이언트로 발급해주게 됨.
    (AccessToken에는 일반적으로 세션 방식과 동일하게 userID정도만 들어감.)

    • accessToken은 당장 데이터들을 받아오는데 사용하게 되는 토큰이고 refreshToken의 경우 만료된 accessToken을 재발급하는데 사용됨.
  • 이처럼 토큰 방식을 이용하게 되면 Session Storage를 사용하지 않기에 확장에도 좋고 서버 부하도 덜하다는 장점이 있지만 토큰을 탈취 당하면 안됨.

    • 그래서 refresh Token의 경우 일반적으로 클라이언트에게 쿠키에 담아줌.
      • 쿠키에 담게 되면 httpOnly속성을 작성할 수 있어서 javascript로 접근해 토큰을 탈취하거나 변조하기 힘들게 됨.
      • 브라우저에서 자동으로 쿠키도 보내주기에 다른 도메인에서 악성 요청을 보내는 시도도 막을 수 있고 XSS나 CSRF공격에 어느정도는 대응하는 셈임.
  • access Token의 경우 일반적으로 쿠키에 담지 않음.

    • access Token의 경우 만료기간이 굉장히 짧은 편인데 만약 사용자의 인증 정보가 변경되거나 만료되는 경우 쿠키를 제어하고 갱신하는 작업이 더 복잡해질 수 있기 때문.
    • 또한 access Token의 길이도 결코 짧지 않기에 브라우저 별로 다른 쿠키 용량에 제한이 걸릴 수도 있음.
  • 그래서 클라이언트는 쿠키를 받은 뒤 다음부터 요청을 보낼 때 클라이언트는 Authorization 헤더에 Bearer 토큰을 담아서 보냄.
    • 백엔드쪽에서는 이를 빼와서 토큰 유효성을 검증하는 식.
  • 이렇게 통신하다보면 accessToken이 만료가 될텐데 만료가 된다면 아래와 같이 동작함.

  • 보다 자세한 방식은 아래 이미지를 참고.

  • 먼저 클라이언트쪽에서 accessToken 재발급을 요청해야되고 백엔드쪽에서는 accessToken이 만료 되었는 지 확인함.
    • 이후 만료된 accessToken을 디코딩 한 뒤 토큰 내 userID를 뽑아와야됨.
      • 왜냐하면 redis에 저장되어있는 refreshToken의 키 값이 userID이기 때문.
    • redis의 userID : refreshToken값의 토큰값과 보낸 토큰 값이 일치하는지를 비교함.
      • 만약 일치한다면 accessToken을 디코딩했던 데이터를 기반으로 백엔드는 새로운 accessToken을 발급해줌.
    • 이후 새로운 accessToken과 refreshToken을 보내주는 방식으로 재발급이 진행됨.

5. 참고.


6. 한 번 읽어볼 글.

profile
Every cloud has a silver lining.

0개의 댓글