JWT (JSON Web Token) 기본 구조와 사용 방법

현정재·2024년 7월 26일
0

JWT (JSON Web Token) 기본 구조와 사용 방법

JWT (JSON Web Token)는 클라이언트와 서버 간의 정보 교환을 위한 컴팩트하고 자가 포함된 방식의 토큰입니다. JWT는 보통 클라이언트가 서버에 인증을 요청할 때 사용됩니다. JWT는 세 가지 주요 부분으로 구성됩니다: Header, Payload, Signature.

JWT의 기본 구조

JWT는 다음과 같은 세 부분으로 구성됩니다:

  1. Header: 토큰의 타입과 서명 알고리즘을 지정합니다.
  2. Payload: 토큰에 포함된 클레임(Claim)을 담고 있습니다. 클레임은 주로 사용자 정보나 기타 메타데이터입니다.
  3. Signature: 토큰의 무결성을 보장하기 위해 사용됩니다. Header와 Payload를 인코딩하고 서명 알고리즘을 적용하여 생성됩니다.

각 부분은 Base64Url 인코딩되어 있으며, 세 부분이 점(.)으로 구분됩니다. 예를 들어:

Header.Payload.Signature

1. Header

Header는 두 부분으로 구성됩니다: algtyp.

  • alg: 사용할 서명 알고리즘 (예: HMAC SHA256 또는 RSA).
  • typ: 토큰의 타입을 지정합니다. 일반적으로 "JWT"를 사용합니다.

예시:

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

Base64Url 인코딩을 거치면 다음과 같은 문자열이 됩니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

Payload는 클레임을 포함하고 있습니다. 클레임의 종류는 크게 세 가지로 나눌 수 있습니다:

  1. 등록된 클레임 (Registered Claims): 토큰에 대한 정보를 제공하기 위해 권장되는 클레임입니다. 예를 들어, iss (발행자), exp (만료 시간), sub (주제), aud (대상자) 등이 있습니다.
  2. 공개 클레임 (Public Claims): 충돌을 방지하기 위해 IANA JSON Web Token Registry에서 정의하거나 충돌이 없도록 정의할 수 있는 클레임입니다.
  3. 비공개 클레임 (Private Claims): 클라이언트와 서버 사이에 동의된 클레임입니다.

예시:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Base64Url 인코딩을 거치면 다음과 같은 문자열이 됩니다:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3. Signature

Signature는 JWT의 무결성을 보장합니다. Header와 Payload를 Base64Url 인코딩하고, 이를 비밀 키로 서명합니다. 예를 들어, HMAC SHA256 알고리즘을 사용할 때 서명은 다음과 같이 생성됩니다:

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

예시:

dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

JWT의 전체 예시

위의 예시를 모두 결합하면 완전한 JWT는 다음과 같습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

이 토큰은 세 부분으로 나뉘며, 각각 Base64Url 인코딩된 Header, Payload, 그리고 Signature입니다.

Spring Boot에서 JWT 토큰 생성 및 검증 유틸리티 클래스 구현

이번 포스팅에서는 Spring Boot 애플리케이션에서 JWT(Json Web Token) 토큰을 생성하고 검증하는 유틸리티 클래스를 작성하는 방법을 다룹니다. 또한 Spring Security를 사용하지 않고도 JWT를 검증하는 HTTP 필터를 구현하는 방법에 대해서도 설명합니다.

JWT 유틸리티 클래스

먼저, JWT 토큰을 생성하고 검증하기 위한 JwtUtil 클래스를 작성합니다.

JwtUtil.java

import io.jsonwebtoken.Claims; // JWT의 클레임(Claims)을 처리하는 클래스
import io.jsonwebtoken.Jwts; // JWT를 생성하고 파싱하는 유틸리티 클래스
import io.jsonwebtoken.SignatureAlgorithm; // JWT에 사용할 서명 알고리즘을 정의하는 클래스
import io.jsonwebtoken.security.Keys; // 안전한 키 생성을 위한 유틸리티 클래스
import jakarta.annotation.PostConstruct; // Spring에서 초기화 작업을 수행하는 메서드에 사용
import lombok.RequiredArgsConstructor; // Lombok을 사용하여 필요한 인자들을 자동으로 생성자 주입
import org.springframework.stereotype.Component; // Spring의 컴포넌트 스캔에 의해 빈으로 등록

import java.security.Key; // 암호화 키를 나타내는 클래스
import java.util.Date; // 날짜 및 시간을 나타내는 클래스

@Component // 이 클래스를 Spring의 컴포넌트로 등록
@RequiredArgsConstructor // final 필드들을 매개변수로 하는 생성자를 자동으로 생성
public class JwtUtil {

    private Key key; // JWT 서명을 위한 키

    @PostConstruct // 의존성 주입이 완료된 후 초기화 작업 수행
    public void init() {
        key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // HMAC-SHA256 알고리즘을 사용하여 비밀 키 생성
    }

    private final int accessTokenExpiration = 3600; // 액세스 토큰의 만료 시간 (1시간)

    // JWT 토큰을 생성하는 메서드
    public String generateToken(String email) {
        return Jwts.builder()
                .setSubject(email) // 토큰의 주체 설정 (이메일)
                .setIssuedAt(new Date()) // 토큰 발행 시간 설정
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration * 1000)) // 토큰 만료 시간 설정
                .signWith(key) // 서명 키 설정
                .compact(); // 토큰 생성
    }

    // JWT 토큰에서 이메일을 추출하는 메서드
    public String getEmailFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key) // 서명 키 설정
                .build()
                .parseClaimsJws(token) // 토큰 파싱
                .getBody(); // 클레임 부분 추출
        return claims.getSubject(); // 주체(이메일) 반환
    }

    // JWT 토큰을 검증하는 메서드
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); // 토큰 파싱 및 검증
            return true; // 검증 성공 시 true 반환
        } catch (Exception e) {
            return false; // 검증 실패 시 false 반환
        }
    }
}

JWT 검증 필터

HTTP 요청을 가로채서 JWT 토큰을 검증하는 필터를 작성합니다.

JwtFilter.java

import jakarta.servlet.FilterChain; // 필터 체인을 나타내는 클래스
import jakarta.servlet.ServletException; // 서블릿 관련 예외 클래스
import jakarta.servlet.http.HttpFilter; // HTTP 요청 필터링을 위한 기본 클래스
import jakarta.servlet.http.HttpServletRequest; // HTTP 요청을 나타내는 클래스
import jakarta.servlet.http.HttpServletResponse; // HTTP 응답을 나타내는 클래스
import lombok.RequiredArgsConstructor; // Lombok을 사용하여 필요한 인자들을 자동으로 생성자 주입
import org.springframework.stereotype.Component; // Spring의 컴포넌트 스캔에 의해 빈으로 등록

import java.io.IOException; // 입출력 관련 예외 클래스

@Component // 이 클래스를 Spring의 컴포넌트로 등록
@RequiredArgsConstructor // final 필드들을 매개변수로 하는 생성자를 자동으로 생성
public class JwtFilter extends HttpFilter {

    private final JwtUtil jwtUtil; // JWT 유틸리티 클래스 주입

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String authorizationHeader = request.getHeader("Authorization"); // Authorization 헤더에서 토큰을 가져옴

        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { // 토큰이 없거나 형식이 올바르지 않은 경우
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized 응답 설정
            response.getWriter().write("Unauthorized: No token provided"); // 응답 메시지 설정
            return;
        }

        String token = authorizationHeader.substring(7); // "Bearer " 부분을 제거하고 토큰만 추출

        if (!jwtUtil.validateToken(token)) { // 토큰이 유효하지 않은 경우
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized 응답 설정
            response.getWriter().write("Unauthorized: Invalid token"); // 응답 메시지 설정
            return;
        }

        String email = jwtUtil.getEmailFromToken(token); // 토큰에서 이메일 추출
        request.setAttribute("email", email); // 요청에 이메일 속성 설정

        chain.doFilter(request, response); // 다음 필터 또는 서블릿 실행
    }
}

필터 등록

Spring Boot에서 필터를 등록하기 위해 설정 파일을 작성합니다.

FilterConfig.java

import org.springframework.boot.web.servlet.FilterRegistrationBean; // 필터 등록을 위한 클래스
import org.springframework.context.annotation.Bean; // 빈 설정을 위한 애너테이션
import org.springframework.context.annotation.Configuration; // 설정 클래스를 나타내는 애너테이션

@Configuration // 이 클래스를 설정 클래스로 등록
public class FilterConfig {

    private final JwtFilter jwtFilter; // JWT 필터 주입

    public FilterConfig(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilterRegistrationBean() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(jwtFilter);
        registrationBean.addUrlPatterns("/api/*"); // JWT 검증을 적용할 URL 패턴
        return registrationBean;
    }
}
profile
wonttock

0개의 댓글