Java | Spring

[Spring Boot] JWT (1) JSON Web Token

@leem 2025. 2. 25. 15:33

JWT(JSON Web Token)

JWT는 사용자 인증을 위한 토큰 기반 방식입니다.
기존의 세션 방식보다 가볍고 확장성이 뛰어나며, 서버 부하가 적습니다.


1. 기존 세션 방식 vs JWT 방식

웹에서 로그인 인증을 할 때, 기존의 세션 방식과 JWT 방식의 차이점을 먼저 살펴보겠습니다.

기존 세션 방식 (Session-based Authentication)

  • 사용자가 로그인하면 서버가 세션을 생성하여 사용자 정보를 저장
  • 이후 요청마다 세션 ID를 포함하여 서버에서 인증 확인
  • 서버가 세션을 저장해야 하므로 확장성이 낮음 (부하 증가!)

JWT 방식 (Token-based Authentication)

  • 사용자가 로그인하면 서버가 JWT(토큰)를 발급
  • 이후 요청마다 JWT를 포함하여 인증
  • 서버가 세션을 저장할 필요 없음 (확장성이 뛰어남!)

💡 JWT는 서버 부하를 줄이면서 인증을 간편하게 처리할 수 있는 방법 

 


2. JWT의 구조 

JWT는 Header(헤더), Payload(페이로드), Signature(서명) 3가지로 구성됩니다.

xxxxx.yyyyy.zzzzz  // JWT 형식

Header (헤더)

JWT의 토큰 타입과 해싱 알고리즘 정보를 포함하는 부분입니다.

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

alg: 사용할 암호화 알고리즘 (예: HS256)
typ: 토큰의 타입 (JWT 사용)

Payload (페이로드)

사용자의 정보와 토큰 만료 시간(Exp) 을 포함하는 부분입니다.

{
  "username": "john_doe",
  "role": "admin",
  "exp": 1700000000
}

username: 사용자의 아이디
role: 사용자의 역할 (예: admin, user)
exp: 토큰 만료 시간 (UNIX Timestamp 형식)

⚠️ 주의

  • Payload는 Base64로 인코딩될 뿐, 암호화되지 않습니다!
  • 비밀번호 같은 민감한 정보는 절대 포함하면 안 됩니다.

Signature (서명)

JWT가 위조되지 않았음을 보장하는 암호화된 해싱 값입니다.

  • 서버가 비밀키(secret)를 사용해 생성
  • 변조된 토큰인지 확인할 때 사용

3. JWT의 동작 방식 

  1. 사용자가 로그인 요청
    사용자가 username과 password를 입력하여 로그인
  2. 서버가 JWT를 생성하여 반환
    서버는 로그인 성공 후 JWT를 생성하여 클라이언트에게 전달
  3. 클라이언트가 JWT를 저장
    클라이언트(브라우저 또는 앱)는 받은 JWT를 로컬스토리지, 세션스토리지 또는 쿠키에 저장
  4. 이후 요청마다 JWT 포함
    사용자는 API 요청을 보낼 때, Authorization 헤더에 JWT를 포함하여 보냄
  5. 서버가 JWT를 검증하여 사용자 확인
    서버는 요청을 받을 때마다 JWT를 해독하여 사용자 정보를 확인
Authorization: Bearer xxxxx.yyyyy.zzzzz
  • 토큰이 위조되었거나 만료되었으면 거부
  • 토큰이 유효하면 요청을 정상 처리
  • 액세스 토큰 → 짧은 만료 시간(10분)으로 설정하여 보안 강화
  • 리프레시 토큰 → 긴 만료 시간(7일)로 설정하여 액세스 토큰 재발급

4. JWT를 활용한 로그인 예제 (Spring Boot) 

1) User 모델 (User.java)

package com.tenco.class_jwt_v01.domain;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String password; // 보안을 위해 암호화 필요
    private String refreshToken; // Refresh Token 저장용 (7일 동안 유지)
}
  • username, password: 사용자의 로그인 정보
  • refreshToken: JWT가 만료되었을 때 새 JWT를 발급받기 위한 토큰

2) UserMapper (UserMapper.java)

package com.tenco.class_jwt_v01.mapper;

import com.tenco.class_jwt_v01.domain.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    void save(User user);  // 회원 저장
    User findByUsername(String username);  // 사용자 정보 조회
    void updateRefreshToken(User user);  // Refresh Token 업데이트
}
  • 연결된 Mapper 인터페이스로, 사용자의 정보를 저장하거나 조회하는 역할

3) JWT 생성 및 검증 (JWTUtil.java)

JWT 생성 및 검증을 담당하는 유틸 클래스

  • generateAccessToken() → 사용자 인증 후 액세스 토큰 발급 (10분 유효)
  • generateRefreshToken() → 액세스 토큰 만료 시 리프레시 토큰 발급 (7일 유효)
  • extractUsername() → JWT에서 username 값 추출
  • extractRole() → JWT에서 role 값 추출
  • validateToken() → 토큰이 유효한지 검사
package com.tenco.class_jwt_v01.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JWTUtil {

    @Value("${jwt.secret}")
    private String secret; // JWT 암호화 키 (환경변수에서 관리)

    // 📌 액세스 토큰 생성 (10분 유효)
    public String generateAccessToken(String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withClaim("role", "USER")  // 역할 정보 포함
                    .withIssuedAt(new Date())  // 발급 시간
                    .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 10))  // 10분 만료
                    .sign(algorithm);
        } catch (JWTCreationException e) {
            throw new RuntimeException("액세스 토큰 생성 실패", e);
        }
    }

    // 📌 Refresh Token 생성 (7일 유효)
    public String generateRefreshToken(String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withClaim("role", "USER")
                    .withIssuedAt(new Date())
                    .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7)) // 7일 만료
                    .sign(algorithm);
        } catch (JWTCreationException e) {
            throw new RuntimeException("리프레시 토큰 생성 실패", e);
        }
    }

    // 📌 JWT에서 username 추출
    public String extractUsername(String token) {
        return JWT.decode(token).getClaim("username").asString();
    }

    // 📌 토큰 유효성 검사
    public boolean validateToken(String token, String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWT.require(algorithm).build().verify(token);  // 검증
            return extractUsername(token).equals(username);  // 사용자가 일치하는지 확인
        } catch (Exception e) {
            return false;
        }
    }
}

생성

    • 액세스 토큰과 리프레시 토큰을 각각 생성
    • HMAC256 알고리즘으로 서명하여 위조 방지
    • withExpiresAt()을 활용하여 만료 시간 설정

데이터 추출

    • JWT의 Payload에서 username과 role 값을 추출하는 메서드
    • Base64로 디코딩하여 쉽게 값을 확인 가능

검증 및 유효성 체크

  • 토큰이 위조되지 않았는지 검증 (JWT.require().verify())
  • 만료 시간을 확인하고, 토큰이 유효하면 true 반환