관리 메뉴

ballqs 님의 블로그

[Spring] JWT(Json Web Tokens)란? 본문

코딩 공부/Spring

[Spring] JWT(Json Web Tokens)란?

ballqs 2024. 8. 19. 20:27

JWT란?

Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim기반의 Web Token이다.

토큰의 한 종류라고 생각하면 된다. 일반적으로 쿠키 저장소를 사용하여 JWT를 저장한다.


JWT 사용이유

기본적으로 클라이언트가 많을 경우 세션의 경우는 아래와 같이 사용된다.

Session storage가 모든 Client의 로그인 정보 소유하고 있기 때문에 모든 서버에서 모든 Client의 API 요청을 처리한다.

단! 이렇게 처리 될 경우 Session storage에 많은 접근이 있어서 과부하가 올 확률이 높다.

그렇게 먹통이 되면 모든 서버가 동작이 안되게 처리되는 것이다.

 

로그인 정보를 Server에 Session에 저장하지 않고 Client에 로그인 정보를 JWT로 HS256인코딩 하여 저장하며 JWT를 통해 인증/인가 하는 방법이다.

모든 서버에서 동일한 Secret Key를 가지고 있고 이를 통해 HS256인코딩 를 하면 된다.


JWT 장/단점

장점

  • 동시 접속자가 많을 때 서버 측 부하 낮춤
  • Client, Sever 가 다른 도메인을 사용할 때
    • 예) 카카오 OAuth2 로그인 시 JWT Token 사용

단점

  • 구현의 복잡도 증가
  • JWT 에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
  • 기 생성된 JWT 를 일부만 만료시킬 방법이 없음
  • Secret key 유출 시 JWT 조작 가능

JWT 구조

  • JWT 는 누구나 평문으로 복호화 가능합니다.
  • 하지만 Secret Key 가 없으면 JWT 수정 불가능합니다.
  • → 결국 JWT 는 Read only 데이터입니다.

 

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

 

payload

{
  "sub": "1234567890",
  "username": "카즈하",
  "admin": true
}

 

Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

적용한 코드

package com.sparta.springauth.jwt;

import com.sparta.springauth.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
    // 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 ";
    // 토큰 만료시간(ms 단위)
    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() {
        // decode를 하면 byte[] 로 반환댐
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

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

        // setSubject 토큰의 용도를 명시한다. (디코딩했을때 값이 제대로 나오면 인가난것)
        // ㄴunique한 값이 일반적으로 들어감
        // claim는 헤더에 보내는 행위 자체를 객체를 만드는 자체를 payload에 담는다.
        // setExpiration 만료 시간을 설정한다.
        // setIssuedAt 생성시간을 설정한다.
        // signWith는 어떤 알고리즘과 key값으로 payload를 SH256방식으로 인코딩한다.
        // compact 토큰을 생성한다.
        return BEARER_PREFIX +
                Jwts.builder()
                    .setSubject(username) // 사용자 식별자값(ID)
                    .claim(AUTHORIZATION_KEY, role) // 사용자 권한 Map<Key , Value>
                    .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                    .setIssuedAt(date) // 발급일
                    .signWith(key, signatureAlgorithm) // 인코딩 알고리즘
                    .compact();
    }


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

            // 쿠키는 사용방법에 권장하지 않음
            // Header에 담거나 차라리 난해하더라도 Session에 담아서 처리
            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
            cookie.setPath("/");

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

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            // "Bearer "이 공백을 포함하여 7자를 자른다.
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    // 토큰 검증
    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;
    }

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

    // HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }

}

 


마무리

보통 Session만 사용하다가 JWT를 처음 배워보는데 많이 복잡했다.

구조 자체가 이해 안가서 여기저기 물어보면서 이해를 했다.