Spring/팀스파르타

27. JWT 다루기

열심히 해 2024. 11. 15. 09:33

JWT란 무엇일까?

JWT(Json Web Token): JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다. 즉, 토큰의 한 종류라고 생각하시면 됩니다. 일반적으로 쿠키 저장소를 사용하여 JWT를 저장합니다.

 

 

JWT 장/단점

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

 

 

JWT 사용 흐름

  • Client 가 username, password 로 로그인 성공 시
    1. 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)
    2. 서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
    3. 브라우저 쿠키 저장소에 자동으로 JWT 저장됨

  • Client 에서 JWT 통해 인증방법
    1. 서버에서 API 요청 시마다 쿠키에 포함된 JWT를 찾아서 사용 - 쿠키에 담긴 정보가 여러 개일 수 있기 때문에 그 중 이름이 JWT가 담긴 쿠키의 이름과 동일한지 확인하여 JWT를 가져옵니다.
    2. Server
      1. Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
      2. JWT 유효기간이 지나지 않았는지 검증
      3. JWT에서 사용자 정보를 가져와 확인

 

 


 

JWT 생성 및 활용

 

0. 토큰 생성에 필요한 데이터

import java.util.Base64;

// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

@PostConstruct
public void init() {
    byte[] bytes = Base64.getDecoder().decode(secretKey);
    key = Keys.hmacShaKeyFor(bytes);
}

 

  • Base64로 Encode된 Secret Key를 properties에 작성해두고 @Value를 통해 가져옵니다.
  • JWT를 생성할 때 가져온 Secret Key로 암호화합니다.
    • 이때 Encode된 Secret Key를 Decode 해서 사용합니다.
    • Key는 Decode된 Secret Key를 담는 객체입니다.
    • @PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용됩니다.
      • 해당 클래스의 생성자 호출 이후에 실행되어 Key 필드에 값을 주입 해줍니다.
  • 암호화 알고리즘은 HS256 알고리즘을 사용합니다.
  • Bearer 이란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시입니다.

 

 

1. JWT 생성

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

    return BEARER_PREFIX +
            Jwts.builder()
                    .setSubject(username) // 사용자 식별자값(ID)
                    .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                    .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                    .setIssuedAt(date) // 발급일
                    .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                    .compact();
}
  • JWT의 subject에 사용자의 식별값 즉, ID를 넣습니다.
  • JWT에 사용자의 권한 정보를 넣습니다. key-value 형식으로 key 값을 통해 확인할 수 있습니다.
  • 토큰 만료시간을 넣습니다. ms 기준입니다.
  • issuedAt에 발급일을 넣습니다.
  • signWith에 secretKey 값을 담고있는 key와 암호화 알고리즘을 값을 넣어줍니다.
    • key와 암호화 알고리즘을 사용하여 JWT를 암호화합니다.

 

 

2. JWT Cookie에 저장

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
    try {
        token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
        cookie.setPath("/");

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        logger.error(e.getMessage());
    }
}

 

 

3.받아온 Cookie의 Value인 JWT 토큰 substring

// JWT 토큰 substring
public String substringToken(String tokenValue) {
    if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
        return tokenValue.substring(7);
    }
    logger.error("Not Found Token");
    throw new NullPointerException("Not Found Token");
}

 

StringUtils.hasText를 사용하여 공백, null을 확인하고 startsWith을 사용하여 토큰의 시작값이 Bearer이 맞는지 확인합니다. 맞다면 순수 JWT를 반환하기 위해 substring을 사용하여 'Bearer '을 잘라냅니다.

 

 

4. JWT 검증

import java.security.SignatureException;

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

 

Jwts.parserBuilder() 를 사용하여 JWT를 파싱할 수 있습니다. JWT가 위변조되지 않았는지 secretKey(key)값을 넣어 확인합니다.

 

 

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

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

 

 

JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있습니다. 여기에 담긴 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 key-value의 한 쌍으로 이뤄져있습니다. 토큰에는 여러 개의 클레임들을 넣을 수 있습니다. Jwts.parserBuilder() 와 secretKey를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자의 정보를 사용합니다.

 

 

 

'Spring > 팀스파르타' 카테고리의 다른 글

30. Spring Security 로그인  (1) 2024.11.20
28. 필터란 무엇일까?  (0) 2024.11.16
26. 쿠키와 세션이란 무엇일까?  (0) 2024.11.13
25. 인증과 인가란 무엇일까?  (1) 2024.11.12
24. Bean을 수동으로 등록하기  (1) 2024.11.10