JWT

JinJinJara·2023년 11월 3일
0

TIL

목록 보기
12/19

JWT

JWT 란?

JWT(Json Web Token)
: Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token으로, 토큰의 한 종류이다.

(보통 쿠키 저장소에 담겨서 위에서 배운 ‘저장된 쿠키’라고 생각하시면 좋습니다.)

JWT 사용 이유

대용량 트래픽 처리를 위해 서버가 2대 이상 필요한 경우가 있다.
이때 Session 마다 다른 Client 로그인 정보를 갖고 있을 수 있다.

🤔❓ 문제
: Client 1 의 로그인 정보를 가지고 있지 않은 Sever2 에게 API 요청을 하게된다면?!

🤓 해결
1. Sticky Session: Client 마다 요청 Server 고정
2. 👍 세션 저장소 생성 or JWT 사용

  • Session storage
    • Session storage 가 모든 Client 의 로그인 정보 소유
  • JWT

    • 로그인 정보를 Server 에 저장하지 않고, Client 에 로그인정보를 JWT 로 암호화하여 저장
      → JWT 통해 인증/인가

    • 모든 서버에서 동일한 Secret Key 소유

    • Secret Key를 통한 암호화 / 위조 검증 (복호화시)

    • JWT 장/단점

    1. 장점
      • 동시 접속자가 많을 때 서버 측 부하 낮춤
      • Client, Sever 가 다른 도메인을 사용할 때
        • 예) 카카오 OAuth2 로그인 시 JWT Token 사용
    2. 단점
      • 구현의 복잡도 증가
      • JWT 에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
      • 기 생성된 JWT 를 일부만 만료시킬 방법이 없음
      • Secret key 유출 시 JWT 조작 가능

추가 공부) Session VS Jwt 어떤게 더 좋을까?

JWT 사용 흐름

  1. Client 로그인 성공

    a. 로그인 정보를 JWT 로 암호화

    b. JWT 를 Client 응답에 전달

    • ex) Authorization: BEARER eyJ0eXAiO~~

    c. Client 에서 JWT 저장 (쿠기, Local storage 등)

  2. Client 에서 JWT 를 통해 인증하는 방법

    a. Client : JWT 를 API 요청 시마다 Header 에 포함

    Content-Type: application/json
    Authorization: Bearer <JWT>
      ...

    b. Server

    • Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
  • JWT 유효기간 검증
  • 검증 성공시, JWT 에서 사용자 정보를 가져와 확인

JWT 구조

  • JWT는 누구나 평문으로 복호화 가능하다.
  • Secret Key가 있어야만 JWT 수정이 가능하다
  • JWT 는 Ready only 데이터이다.

JWT 구현하기

JWT dependency

compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

application.yml

spring:
  ...
  jwt:
    secret:
      key=🔒

1. 토큰 생성에 필요한 값들

    // Header Key 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    
    // 사용자 권한 값의 Key
    public static final String AUTHORIZATION_KEY = "auth";
    
    // Token 식별자
        /* Bearer : OAuth 2.0 인증 프로토콜에서 사용하는 인증 토큰의 유형을 명시하는 문자열
           Bearer 다음에 오는 토큰 값은 하나의 문자열로 인식되어야 하기 때문에,
           Bearer 와 토큰 값 사이에는 한 칸 띄어쓰기가 필요
       */
    private static final String BEARER_PREFIX = "Bearer ";
    
    // 토큰 만료 시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;
    
    // JWT 의 비밀 키 저장
    @Value("${jwt.secret.key}")
    private String secretKey;
    
    // JWT 서명에 사용할 비밀 키를 저장
    private Key key;
    
    // JWT 서명 알고리즘을 정의하는 열거형 변수 <HS256 (HMAC with SHA-256) 알고리즘>
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    
    // 애플리케이션 시작 시 비밀 키를 디코딩하고 Key 객체로 변환하는 초기화 작업을 수행
    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }
    
@Value("${jwt.secret.key}")
private String secretKey;
  • Spring 의존성 주입을 위해 사용됨
    : application.yml (애플리케이션 설정 파일) 에 정의된 속성 이름으로 JWT 의 비밀 키를 저장

JWT 서명에 필요한 키를 초기화하고, 해당 서명 알고리즘을 설정하는 데 사용

@PostConstruct
public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }
  • @PostConstruct : 종속성 주입이 완료된 후 실행되어야 하는 메서드에 사용
  • init() : secretKey로부터 바이트 배열을 디코딩하고, 이 바이트 배열을 사용하여 Keys.hmacShaKeyFor() 메서드를 호출하여 Key 객체를 생성

2. Header 에서 Token 가져오기


   // header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }
  • 요청 헤더에서 Authorization 값을 읽어온 후 7개 문자열(=Bearer )을 제거한다.

3. JWT 생성

   // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }
  • setSubject : JWT 의 주체 설정
  • claim : 역할 정보를 claim으로 추가
  • setExpriation : JWT 만료 시간 설정
  • setIssuedAt : JWT 발급 시간
  • signWith : JWT를 서명하는데 사용할 키와 알고리즘
    • 서명 : JWT 의 무결성을 보장
  • compact() : JWT 를 문자열로 변환 후 반환

4. JWT 검증

   // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
  • JWT parser 를 빌드하고, 서명에 사용된 키를 설정하고, JWT 토큰을 Parsing 해서 claim 정보를 가져온다.

5. JWT 에서 사용자 정보 가져오기

   // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
  • 전달받은 JWT 토큰에서 claim 정보를 추출하고 해당 정보를 Claims 객체로 반환

전체 코드

package <com.example.demo.domain.utility.jwt;

import com.example.demo.domain.member.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

    /**
     * 토큰 생성에 필요한 값들
     */
    
    /* Header Key 값 */
    public static final String AUTHORIZATION_HEADER = "Authorization";
    
    // 사용자 권한 값의 Key
    public static final String AUTHORIZATION_KEY = "auth";
    
    // Token 식별자
        // Bearer : OAuth 2.0 인증 프로토콜에서 사용하는 인증 토큰의 유형을 명시하는 문자열
        // Bearer 다음에 오는 토큰 값은 하나의 문자열로 인식되어야 하기 때문에,
        // Bearer 와 토큰 값 사이에는 한 칸 띄어쓰기가 필요
    private static final String BEARER_PREFIX = "Bearer ";

    // 토큰 만료 시간
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    // JWT 의 비밀 키 저장
    @Value("${jwt.secret.key}")
    private String secretKey;

    // JWT 서명에 사용할 비밀 키를 저장
    private Key key;

    // JWT 서명 알고리즘을 정의하는 열거형 변수 <HS256 (HMAC with SHA-256) 알고리즘>
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 애플리케이션 시작 시 비밀 키를 디코딩하고 Key 객체로 변환하는 초기화 작업을 수행
    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    /**
     *  Header 에서 Token 가져오기
     */
    // header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    /**
     * JWT 생성
     */
    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }

    /**
     * JWT 검증
     */
    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    /**
     * JWT 에서 사용자 정보 가져오기
     */
    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

}

0개의 댓글