JWT, Web Socket 정리

Jingu_Jeon·2024년 10월 15일

PHCCS의 개발된 JWT와 Web Socket 정리를 하고자함.

JWT(Json Web Token)

JWT란?

  • JSON 포맨을 기반으로 한 웹 표준 토큰

왜 사용하는가?

  • 보안
    • 토큰에는 서명이 포함됨 -> 위조 여부 판단 가능
    • 또한 서명은 대칭키 또는 비대칭키 암호화
    • 토큰에는 유효기간이 있어 일정 시간이 되면 만료되어 사용할 수 없음
      -> 보안성 증가
  • 사용자 정보를 포함
    • 토큰 생성시 페이로드에 사용자 정보를 포함 할 수 있음
      • ex) 권한을 포함시켜 리소스에 대한 사용자의 접근 여부를 판단 할 수 있음
      • ex) 포함된 식별자 memberId를 db의 사용자 정보를 빠르게 조회할 수 있음
  • 무상태(stateless) 인증
    • 서버가 토큰을 저장하지않고 서명 확인을 통해 토큰이 유효한지를 판단하여 인증
    • 무상태 원칙으로 작동하는 RESTful API 인증 가능
  • AccessToken 과 RefreshToken
    • AccessToken
      :짧은 유효 기간 동안 클라이언트의 인증을 처리하며, 주로 API 요청을 위해 사용됨
    • RefreshToken
      : Access Token이 만료되었을 때, 클라이언트가 새로운 Access Token을 발급받을 수 있도록 사용되며, 더 긴 유효 기간을 가짐
    • 두 토큰을 사용함으로써 보안성과 사용자 경험을 균형있게 유지 가능
      • 보안
        : Refresh Token을 통해 재발급함으로써, 짧은 주기의 Access Token이 유출 되더라도 보안 위험을 최소화할 수 있음.
      • 사용자 경험
        : Access Token이 만료될 때마다 로그인을 반복할 필요 없이, Refresh Token으로 새로운 Access Token을 발급받아 세션을 유지할 수 있음.

PHCCS에서의 적용

의존성 추가

  • build.gradle 의 JJWT 의존성 추가 코드부분
  • implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    : JWT생성, 파싱, 검증을 위한 API 제공
  • implementation 'io.jsonwebtoken:jjwt-impl:0.11.5''
    : JWT API의 구체적인 구현체로 실제 JWT 처리
  • implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    : JSON 데이터를 Jackson 라이브러리로 처리할 수 있게 해줌.

JWT 속성 정의

  • applicatin-jwt-properties 코드
  • jwt.issuer
    : JWT 발급자를 정의
  • jwt.secret-key
    : JWT 서명에 사용되는 비밀키
  • jwt.access-token-expriation
    : AccessToken의 유효 시간 정의
  • jwt.refresh-token-expiration
    : RefreshToken의 유효 시간 정의
  • 해당 속성은 JwtProperties 클래스에서 바인딩 받아 사용

토큰 생성

1. MemberService의 login

  • 이메일과 비밀번호를 통해 사용자을 검증한 후, 성공적으로 검증되면 JWT 토큰을 생성하여 클라이언트에게 반환
  • jwtUtil.createAccessToken(member.getId(), member.getRole())
    • 사용자의 ID와 역할(role)을 기반으로 AccessToken을 생성
  • jwtUtil.createRefreshToken(member.getId())
    • 사용자 ID를 포함하는 RefreshToken 생성
  • tokenService.storeRefreshToken(jwtUtil.extractId(refreshToken), jwtUtil.actual(refreshToken))
    • RefreshToken을 서버 측에 저장
    • 이 과정을 통해 AccessToken 재발급 요청이 올때 서버는 받은 RefreshToken과 저장된 RefreshToken을 비교해 유효성을 확인하고 새로운 Access Token을 발급함

2. CreateAccessToken과 CreateRefreshToken

  • 다른 차이가 있는 JWT 생성을 위해 두 메소드를 구별함.
    • 차이점
      • claims
        : AcessToken은 claims에 사용자 id와 역할(role)을 포함하지만 RefreshToken은 사용자 id 만 포함
      • 만료시간
        : AcessToken은 비교적 짧은 만료시간, refreshToken은 비교적 긴 만료시간을 가짐
  • createAccessToken(Long id, int role)
    • 멤버ID와 역할(role)을 받아서 해당 정보를 포함한 JWT를 생성
      • Map<String, Object> claims = new HashMap<>()
        : JWT 페이로드에 넣을 클레임을 저장하는 맵을 생성
      • claims.put("role", role)
        : 사용자의 역할(role)을 클레임으로 추가
      • createToken(claims, id.toString(), jwtProperties.getAccessTokenExpiration())
        : createToken() 메서드를 호출하여 JWT 토큰을 생성합니다.
  • createRefreshToken(Long id)
    • Claims에 역할 정보를 추가하지 않고, 주로 멤버ID와 만료시간을 기반으로 생성됨.
      • Map<String, Object> claims = new HashMap<>()
        : Refresh Token에서는 특별한 클레임이 없으므로 빈 맵을 생성
      • createToken(claims, id.toString(), jwtProperties.getRefreshTokenExpiration())
        : createToken() 메서드를 사용하여 JWT 토큰을 생성

3. CreateToken()

  • JWT 를 생성하는 메서드로, Claims, Subject, 만료 시간등의 정보를 기반으로 JWT를 생성하고 서명하는 역할을 함.

  • String jti - UUID.randomUUID().toString()

    • JTI는 JWT의 고유 식별자
    • UUID.randomUUID()을 통해 고유한 UUDI 값을 생성
  • Jwts.builder()

    • JJWT 라이브러리에서 JWT를 생성하기 위한 빌더 패턴의 메서드
    • JWT의 헤더(Header), 페이로드(Patload), 서명(Signature) 등을 설정하고 최종적으로 JWT 토큰을 문자열로 생성
  • setClaims(claims)

    • JWT의 페이로드 부분에 포함될 클레임(claims)을 설정
    • claims는 사용자 정보(예: 사용자 권한, 이메일 등)을 포함할 수 있는 Map(String, Object> 형식의 데이터, 이 claims는 서버에서 확인 가능
  • setId(jti)

    • JWT에 고유한 식별자인 JTI(JWT ID)를 설정
  • setSubject(subject)

    • JWT의 주제(Subject)를 설정
    • 주제는 보통 사용자를 고유하게 식별하는 정보 포함
    • 해당 프로젝트에서는 Subject를 멤버ID(번호)로 설정
  • SetIssuedAt(new Date(System.currentTimeMillis()))

    • JWT가 발급된 시간을 설정
    • 현재 시간을 기준으로 발급된 시점
    • 해당 프로젝트에서 시간기준은 로컬타임(서울시간)
  • setExpiration(new Date(System.currentTimeMillis() + expirationTime))

    • JWT의 만료 시간(Expiration)을 설정
    • 현재 시간에 expirationTime(밀리초 단위)만큼 더한 값을 만료 시간으로 설정
    • Access Token과 Refresh Token 각각에 맞는 유효 시간을 설정
  • signWith(SignatureAlgorithm.HS256, getSigningKey(jwtProperties.getSecretKey()))

    • JWT에 서명을 추가
    • HMAC SHA-256(HS256) 알고리즘을 사용하여 서명하며, 서명에 사용할 비밀키(secret key)는 jwtProperties.getSecretKey()에서 가져옴.
    • getSigningKey() 메서드는 서명에 사용될 비밀키를 반환하는 역할을 함. 이 서명을 통해 JWT가 변조되지 않았음을 보장 가능 함.
  • compact()

    • 모든 정보가 설정되면 compact()를 호출하여 JWT를 문자열로 변환
    • 이 문자열이 최종적으로 클라이언트에 전달
    • 최종적으로 생성된 JWT는 헤더(header), 페이로드(payload), 서명(signature)로 구성된 문자열

JWT 토큰 생성 흐름 정리

1. 사용자 검증 (이메일 및 비밀번호 확인)

findMemberByEmail() 메서드를 사용하여, 클라이언트가 입력한 이메일로 회원 정보 를 데이터베이스에서 검색합니다.
회원이 존재하지 않으면 "회원을 찾을 수 없음"이라는 오류를 발생시킵니다.
회원이 존재하면, 사용자가 입력한 비밀번호와 데이터베이스에 저장된 비밀번호가 일치하는지 확인합니다.
비밀번호가 일치하지 않으면 "검증되지 않음"이라는 오류를 발생시킵니다.

2. Access Token 생성

사용자가 성공적으로 검증되면, 서버는 Access Token을 생성합니다.
jwtUtil.createAccessToken() 메서드를 사용하여 Access Token을 생성합니다.
Access Token은 JWT 형식으로 발급되며, 다음과 같은 정보들이 포함됩니다:
사용자의 ID: Access Token의 주제(subject)로, 주로 사용자 ID가 설정됩니다.
역할(role): 사용자의 권한 또는 역할 정보가 클레임(claims)으로 포함됩니다.
유효 기간: Access Token의 유효 시간은 jwt.access-token-expiration 설정에 따라 600,000밀리초(10분)으로 설정됩니다.
Access Token은 클라이언트가 인증된 요청을 보낼 때 사용하는 토큰으로, 짧은 유효 기간 동안 유효하며, 각 요청에 사용됩니다.

3. Refresh Token 생성 및 저장

Access Token과 함께 Refresh Token도 생성됩니다.
jwtUtil.createRefreshToken() 메서드를 사용하여 Refresh Token을 생성합니다.
Refresh Token은 사용자의 ID만을 포함하며, 유효 기간은 jwt.refresh-token-expiration 설정에 따라 7일로 설정됩니다.
Refresh Token은 Access Token이 만료되었을 때, 새로운 Access Token을 발급받기 위해 사용됩니다.

  • 서버 측 저장:
    Refresh Token은 서버 측에서 관리됩니다. tokenService.storeRefreshToken() 메
    서드를 사용하여 토큰의 고유 ID(JTI)와 토큰 값을 데이터베이스에 저장합니다.
    이 과정은 나중에 클라이언트가 Refresh Token을 사용해 Access Token을 갱신할 때, 서버에서 Refresh Token이 유효한지 검증하기 위해 필요합니다.

4. Access Token과 Refresh Token 반환

Access Token과 Refresh Token이 생성되면, 이 두 토큰을 클라이언트에 반환합니다.
Access Token: 사용자가 인증된 요청을 서버에 보낼 때 사용됩니다.
Refresh Token: 사용자가 Access Token이 만료되었을 때 새로운 Access Token을 요청할 수 있게 해줍니다.
이 두 토큰은 클라이언트 측에서 관리되며, 클라이언트는 Access Token이 만료될 경우 Refresh Token을 사용해 새로운 Access Token을 요청할 수 있습니다.

작동 사진 또는 동영상

토큰 검증

1. JwtAuthenticationFilter

  • Spring 애플리케이션에서 Filter의 작동
    • Spring 애플리케이션에서 필터는 서블릿 필터 체인의 일부로 동작
    • Controller 나 Service 레이어의 로직이 실행되기 전에 요청을 가로챔
    • JwtAuthenticationFilter는 Controller와 Service 로직이 실행되기전에 토큰의 유효성 검사를 진행함
    • 클라이언트 요청 → 필터 체인 → JWT 필터 → 컨트롤러 → 서비스
  • Spring 애플리케이션에서 JwtAuthenticationFilter의 작동
    1. 클라이언트의 요청이 서버로 전송됨.
    2. JWT 인증 필터(JwtAuthenticationFilter)가 요청을 가로채고, JWT 토큰이 유효한지 검증, 이 과정에서 토큰이 유효하지 않으면 바로 응답을 반환하고 요청 처리를 중단함.
      • 필터에서 토큰이 유효하지 않다고 판단되면, 더 이상의 처리가 진행되지 않으며, HTTP 응답이 반환됩니다. (예: 401 Unauthorized)
    3. 필터를 통과한 요청에 대해, 인증이 성공하면 필터는 다음 단계로 요청을 전달함.
    4. 컨트롤러 및 서비스: 필터를 통과한 요청은 이제 MemberService의 update 메 서드와 같은 비즈니스 로직으로 전달됨. 여기서 JWT 토큰에서 추출된 사용자 정보가 활용됨.



  • 필터의 주요 흐름

    1. 화이트리스트 체크

      • isLoginCheckPath() 메서드를 통해 요청 URI가 인증이 필요하지 않은 경로인지 확인함.
      • 화이트리스트에 포함되지 않은 경로의 경우에만 JWT 검증을 수행
      • 해당 프로젝트에서는 "/", "/auth/signup", "/auth/signin", "/auth/refresh", "/css/*" 를 제외한 경로의 경우에만 JWT 검증을 수행
    2. Authoriztion 헤더 검증

      • Authorization 헤더에 JWT 토큰이 존재하는지 확인
      • Bearer 로 시작하는지 확인
      • 헤더가 없거나 형식이 맞니 않으면 401 UNAUTHORIZED 상태 코드를 반환
    3. JWT 토큰 검증

      • JwtUtil 클래스의 validateAccessToken() 메서드를 호출하여 JWT 토큰의 유효성을 확인.
      • 유효한 토큰일 경우 jwtUtil.extractSubject(token) 메서드를 사용하여 토큰에서 MemberId 정보를 추출하고, 이를 요청 속성(attribute)에 저장.
        • attribute에 저장한 MemberID는 컨트롤러 레이어에서 추출해 사용 가능
      • 토큰이 만료되었거나, 서명이 잘못되었거나, 형식이 틀린 경우 각각의 예외에 맞는 상태 코드와 메시지를 반환하고 요청을 차단.
    4. 요청 처리

      • JWT 검증이 통과되면 다음 필터로 요청을 전달하고, 그렇지 않으면 즉시 응답을 반환

2. jwtUtill 클래스의 validateAccessToken() 과 extractAllClaims()

  • 앞서 JwtAuthenticationFilter의 토큰 검증 과정에서 JwtUtil의 ValidateAccessToken()을 사용함
  • 그리고 ValidateAccessToken() 메서드 또한 검증을 위해 extractAllClaims()을 사용함.
  • 이때 extractAllClaims() 메서드가 토큰을 검증하는 과정을 추가로 설명하고자 함.
  1. validateAccessToken()
    1. 토큰 파싱 및 유효성 검사
      • extractAllClaims(token) 메서드를 통해 JWT 토큰의 페이로드를 파싱하고, 토큰의 유효성을 검사.
      • 유효한 경우 TokenStatus.VALID를 반환
    2. 예외처리
      • 만료된 토큰
      • 서명 오류
      • 잘못된 형식의 토큰
      • 알 수 없는 오류
  2. extractAllClaims()
    • JWT 의 유효성을 검증하고 유효한 경우 토큰에서 클레임을 추출하는 역할
    • 해당 메소드가 어떻게 토큰의 유효성을 검사하는지 설명하고자 함.
    • 토큰 파싱
      • 유효성 검사의 핵심은 Jwts.parserBuilder()
      • JWT 토큰을 파싱하고, 내부의 클레임을 추출함.
      • 이 과정에서 토큰의 서명을 검증하는 과정이 포함됨.
      1. 서명 키 설정
        • .setSigningKey(getSigningKey(jwtProperties.getSecretKey()))
        • setSigningkey() 메소드를 사용해 서명에 사용된 비밀키(Secret Key)를 가져옴. 이 비밀 키는 토큰을 발급할 때 서명에 사용한 것과 동일한 키
        • 비밀키를 이용해 JWT의 서명이 유효한지를 확인 만약 서명이 변경되거나 서명에 사용된 키가 다르면, 이 과정에서 오류가 발생 이는 곧 토큰이 변조 되었음을 의미
      2. 토큰 파싱 및 서명 검증
        • .build()
          .parseClaimsJws(actualToken)
        • parseClaimsJws()는 JWT 토큰을 파싱하고 서명을 검증하는 메소드
        • 해당 메소드는 토큰을 디코딩하고, 서명 검증 과정을 거침.
        • 만약 서명이 유효하지 않거나, 잘못된 형식의 토큰일 경우, 예외가 발생
      3. 클레임 추출
        • getBody()
        • 해당 메소드는 서명 검증이 성공적으로 이루어지면, 토큰의 페이로드에 담긴 클레임을 추출
    • 정리
      extractAllClaims() 메소드는 Jwts.parseBuilder()의 메소드 setSigningKey로 비밀키를 가져오고 parseClaimsJws를 통해 JWT를 파싱하고 가져온 키로 서명이 유효한지 검증한다.
  3. MemberService의 update 메서드
    • JwtAuthenticationFilter을 통한 인증 과정을 거치고 JWT 이 service 레이어 에서 어떻게 작동하는지 예시 Memberservice의 update 메서드를 통해 설명하고자 한다.
    • MemberService의 update 메서드는 Authorization 헤더로 전달된 JWT 토큰을 통해 사용자 정보를 검증한 후, 사용자 정보를 수정
    1. 사용자 ID 추출 및 정보 수정
      • jwtUtil.extractSubject()을 통해 토큰에서 사용자 ID를 추출
      • 해당 ID를 바탕으로 service.modifyMember() 메서드를 호출하여 사용자 정보를 수정
    2. 결과 반환
      • 수정이 성공하면 200 OK 응답을, 실패하면 400 Bad Request 응답을 반환

작동 사진 또는 동영상

AccessToken 재발급

** 추후에 추가

profile
Back-end Developer를 목표로 하고 있는 전진구입니다.

0개의 댓글