[Network] JWT(JSON Web Token)

과꼴 취준·2025년 10월 24일

Network

목록 보기
5/5
post-thumbnail

해당 포스트는 https://www.youtube.com/watch?v=36lpDzQzVXs 를 보고 정리한 내용입니다 생활코딩

1. JWT 의 3요소(header, Payload, Signature)


이미지 출처 https://www.youtube.com/watch?v=36lpDzQzVXs
  • header : JWT 의 여러 정보가 있는 공간, Signature 를 만드는 방법에는 여러 방법(알고리즘) 이 존재하는데, 그 알고리즘의 정보를 저장한다.
  • Payload : 실제 JWT 의 내용(본문) 이 포함된 공간.
    • Payload 의 각 부분을 Claim 이라고 하는데, Claim(주장) 이라고 부르는 이유는 Payload 를 순순히 정보로 받는 것이 아닌(주장) 서버가 발행해준 JWT 가 맞는지 검증 을 거처야하기 때문이다.
  • Signature : 서버가 발행 해준 JWT 가 맞는지, 위조되지 않았는지 검증을 하기위한 서명(Signature) 을 포함한다.

3요소(header, Payload, Signature)base64encoding 을 한후 .(마침표) 로 연결시켜주면 우리가 아는 JWT 가 된다

2. signature(서명) 의 핵심 원리


  • 먼저, 서명이 오면 서명을 받는 쪽에서 확인할수있는 무언가(key)가 필요하다.
    서버에서는 이것을 secret key라는 이름으로 가지고 있는다.

  • 클라이언트에게서 login (Authentication) 요청이 들어오면, 애플리케이션 서버는
    서명을 만드는 함수(HMAC) 에다가 Payload , secret Key (서버가 가지고있는) 그에맞는 서명(signature) 을 리턴한다.


  • 해당 클라이언트에서 로그인을 유지한상태 요청 (Authorization) JWT 토큰이 오면, 이 토큰의 header 에 있는 HMAC 알고리즘 정보로 HMAC 함수를 만들어서 서버가 가지고 있는 secret Key 값과, Payload 에 적용시켜서 리턴된 signature 와, JWT 의 토큰안에 들어있는 signature 를 비교해서
    같으면 서버가 발급한 토큰(위조안됨) 으로 판단하고, 다르다면 토큰이 위조되었다고 판단한다.

3. JWT 기반 인증·인가 구조


3.1 JWT 인증(Authentication)

  • 클라이언트(Browser)login 시도를 한다, 서버는 DB 에 Id, password 와 일치하는 회원이 있는지 확인한다.

  • 있으면, 서버의 Secret Key, Payload ,header알고리즘 (HS256)을 통해 signature 를 만들고, Base64 로 인코딩 한후 header.payload.signature
    로 JWT 를 만든다

  • 서버는 발급한 JWT응답으로 클라이언트에 전송한다.
    (전송 방법은 1) 쿠키를 통한 전송, 또는 2) HTTP 메시지 헤더의 Authorization 필드를 사용하는 방식이 있다.)

  • 클라이언트는 받은 JWT를 브라우저의 쿠키 저장소sessionStorage에 저장하고,
    이후 요청마다 이 토큰을 함께 전송함으로써 로그인 상태(Stateful) 를 유지한다.

3.2 JWT 인가(Authorization)

  • login 후, 브라우저가 요청을 보내면, 요청에 JWT 를 담아서 보내게 된다.

  • 서버가 요청을 받으면 JWT 를 꺼낸후, JWTsignaturePayloadsecret key 를 통해 만든 signature 를 통해 검증한다. 검증에 통과가 되면 서버가 클라이언트가 보낸 요청에 대한 처리를 해줘서 응답으로 보내게 된다.

  • JWT 를 사용하면 로그인할때 DB 에 딱 1번만 접근하고, JWT 를 발급받은 후(인가)에선
    DB 에 접근을 하지않아서 서버에 부담을 줄인다.

4. JWTUtils 구현하기


JWT 를 쿠키안에 넣어서 인증(Authentication) 처리를 해보자

1. 상수값 세팅

  • AUTHORIZATION_HEADER : 쿠키의 name 값 (namekey 형식으로 사용해서 value 를 꺼내온다)

  • AUTHORIZATION_KEY: 사용자 권한 값을 가지고 오기 위한 key
  • BEARER_PREFIX : 토큰 식별자. 토큰 앞에는 BEARER(공백) 을 붙이는것이 관례이다.
  • TOKEN_TIME : 토큰 만료 시간
  • secretKey : 서버가 가지고 있는 secretKey
  • signatureAlgorithm : 암호화 알고리즘. 요즘은 HS256 를 대부분 사용한다.

2. 주요 메서드

  • createToken : username, role(권한) 과 세팅해준 상수값과 jwts.bulider() 를 통해 JWT 토큰을 만든다.
  • addJwtToCookie : 생성해준 JWT 를 응답 메세지의 Cookie 에 담아주는 메서드
  • validateToken : io.jsonwebtoken.Jwts 에서 제공해주는 메서드들로 토큰이 위조된건지, 만료되었는지 알수있다
package com.example.springauth.jwt;

import io.jsonwebtoken.*;
import com.example.springauth.Entity.UserRoleEnum;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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;

/**
 * Util 는 특정 파라메터에 대한 작업을 수행하는 메서드들을 모아둔 클래스.
 * 다른 객체에 의존하지 않는다.
 * ex) String $ 로 반환
 */

@Component
public class JwtUtil {
    // Header KEY 값

    public static final String AUTHORIZATION_HEADER = "Authorization";//쿠키의 name 값
    // 사용자 권한 값의 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분
    /**
     * application.properties 에서 설정한 key 값으로 SecretKey 의 value 를 가지고 온다
     */
    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey;

    private Key key; //@PostConstruct 에서 초기화

    //암호화 알고리즘
    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 로 인코딩 되어 있던 문자열을 다시 원래의 바이트 배열로 복원하는 과정(이게 진짜 secretKey 의 원본) ex 우리가 원래 byte 배열을 etf-8 로 encoding 해서 한국어로 만드는것처럼
    }
    //여기까지 데이터 준비코드

    /**
     * 이 코드를 실행하면
     * JWT 의
     * header, payload, signature 를 알잘딱 만들어주고, JWT 를 만들어준다.
     * signWith() 로 signature 로 만드려면 byte 코드로 디코딩 되어 있어야 한다
     */
    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) //signature 자동 생성
                        .compact(); //리턴값은 String (최종 JWT 문자열 완성 코드)
    }

    // 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());
        }
    }

    /**
     * JWT 토큰의(String) 에서 Bearer_ 만큼을 제외한 SubString 이 필요하다
     */

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //null 인지 아닌지, BEARER_PREFIX로 시작하는지 안하는지
            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;
    }

    // 토큰에서 사용자 정보 가져오기 Payload 가지고 오기
    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(); //하면 Cookie[] 를 반환한다.
        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;
    }
}
/**
 * 시크릿 키는 서버에 존재해야 한다
 * 시그니쳐는 header+payload+secretKey 의 3개를 디코딩(바이트 코드로) 한후, 특정 시그니처 알고리즘으로 조합해서 사용한다
 * 이것으로 토큰의 위조 유무를 파악할수 있다.
 */
profile
취업 엑조디아 모으기

0개의 댓글