1. JwtUtil 클래스의 역할
1️⃣ 토큰 생성
사용자 인증 정보를 기반으로 JWT 토큰을 생성합니다.
토큰을 생성할 때, 필요한 Claim을 포함하고, Signature를 통해 토큰의 무결성을 보장합니다.
2️⃣ 토큰 검증
클라이언트로부터 전달 받은 JWT토큰의 유효성을 확인합니다.
토큰의 만료시간, 서명, 클레임 정보등이 포함되어, 토큰이 위조나 변조되지 않았는지 확인합니다.
3️⃣ 토큰 파싱
JWT 토큰에 포함된 정보를 추출(파싱)하여, 사용자 식별 정보나 권한 같은 데이터를 반환합니다.
이를 통해 애플리케이션 내에서 사용자 인증 및 권한 부여를 효율적으로 처리할 수 있습니다.
2. JwtUtil 클래스 생성하기
(1) 필드 설정하기
[JwtUtil 필드]
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
1️⃣ BEARER_PREFIX : JWT 토큰의 접두사(Bearer )
private static final String BEARER_PREFIX = "Bearer ";
JWT는 HTTP 요청의 Authorization 헤더에 `Bearer <토큰값>` 형식으로 포함되어, 인증 방식을 명확히 합니다.
2️⃣ TOKEN_TIME : 토큰 유효 시간 설정 (60분 = 1시간)
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
60 * 60 * 1000L은 1시간(3600000ms)을 의미하며,
이 기간 동안만 토큰이 유효하고 이후에는 재발급이 필요합니다.
3️⃣ secreteKey: JWT 서명(Signature) 키 설정
@Value("${jwt.secrete.key}")
private String secreteKey;
`@Value(”${jwt.secrete.key}”)`는 설정 파일에서 비밀키를 주입받으며,
이 키로 JWT 서명을 생성하여 토큰의 무결성을 보장합니다.
4️⃣ key : JWT 서명 키 객체
private Key key;
String 타입의 secreteKey는 암호화에 사용할 Key 객체로 변환되어 저장됩니다.
5️⃣ signatureAlgorithm : JWT 서명 알고리즘 선택
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
HS256 (HMAC SHA-256)은 대칭키 기반의 서명 알고리즘으로,
토큰 발급과 검증 시 동일한 비밀키를 사용하며,
보안성과 속도의 균형이 우수해 JWT에서 널리 사용됩니다.
(2) JWT 서명을 위한 키를 초기화 하는 메소드
[키를 초기화 하는 메소드 : init()]
@PostConstruct
public void init(){
byte[] bytes = Base64.getDecoder().decode(secreteKey);
key = Keys.hmacShaKeyFor(bytes);
}
🤔 키 초기화 하는 메소드가 필요한 이유
JWT 서명을 위한 키 초기화 메소드는 토큰 변조를 방지하고 무결성을 검증하기 위해 필요합니다.
서버 실행 시, Base64 인코딩된 비밀 키를 디코딩하여
Key 객체로 변환하는 init() 메서드를 한 번만 실행함으로써
보안성과 성능을 높이고, 불필요한 키 재생성을 방지합니다.
1️⃣ `@PostContstruct`
Spring Bean이 생성되고 의존성 주입이 완료된 후,
`@PostConstruct` 어노테이션 덕분에 초기화 메서드가 한 번만 자동 실행되어
secreteKey 값을 기반으로 Key 객체를 초기화합니다.
2️⃣ Base64 디코딩을 통한 안전한 비밀키 관리
byte[] bytes = Base64.getDecoder().decode(secreteKey);
설정 파일에서 가져온 Base64 인코딩된 문자열인 secreteKey를 디코딩하여
원래의 바이트 배열로 변환함으로써 보안성을 높입니다.
3️⃣ JWT 서명용 Key 객체 생성 및 재사용
key = Keys.hmacShaKeyFor(bytes);
디코딩된 바이트 배열을 사용해 JWT 서명에 사용할 Key 객체를 생성합니다.
이 방법은 HMAC SHA-256 알고리즘의 대칭키 특성에 따라 서명 생성과 검증에 동일한 키를 사용하며,
키를 한 번만 생성해 재사용함으로써 보안성과 성능을 모두 높입니다.
(3) 사용자 정보 기반으로 JWT 생성하는 메소드
[JWT 생성하는 메소드 : createToken()]
public String createToken(Long userId, String email, UserRole userRole){
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email",email)
.claim("userRole",userRole.getUserRole())
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key,signatureAlgorithm)
.compact();
}
1️⃣ JWT 시간 정보 설정
Date date = new Date();
현재 시간을 가져와 JWT의 발급 시간(issuedAt)과 만료 시간(expiration)을 설정하는 데 사용됩니다.
2️⃣ JWT 반환 형식
return BEARER_PREFIX + ...
JWT 생성 시, `Bearer` 접두사가 JWT 앞에 붙어 반환되도록 합니다.
이는 HTTP 요청의 Authorization 헤더에서 인증 방식을 명확하게 하기 위해서입니다.
3️⃣ JWT 생성
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email",email)
.claim("userRole",userRole.getUserRole())
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
(4) 순수한 토큰만 반환하는 메소드
[토큰만 반환하는 메소드 : subStringToken()]
public String subStringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("No Found Token");
}
1️⃣ JWT 입력값 검증
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)
이 메소드에서는 입력된 tokenValue가 올바른 JWT 토큰 형식을 따르는지 검증합니다.
`StringUtils.hasText(tokenValue)` 메소드로, 입력된 tokenValue가 null, 빈 문자열 또는 공백이 아닌지 확인합니다.
`tokenValue.startsWith(BEARER_PREFIX)`를 통해 tokenValue가 “Bearer “로 시작하는지 검사합니다.
이 조건을 만족하면 JWT 토큰이 올바르게 전달되었음을 의미합니다.
2️⃣ Bearer 접두사 제거 및 JWT 추출
return tokenValue.substring(7);
입력된 문자열에서 “Bearer “ 접두사를 제거해 순수한 JWT 토큰만 추출합니다.
여기서 substring(7) 메소드는 7글자인 “Bearer “ 이후의 문자열(8번째 문자부터)을 반환합니다.
예를 들어, “Bearer eyJhbGciOiJI…“가 전달되면, “eyJhbGciOiJI…“만 추출됩니다.
3️⃣ 예외 처리 및 오류 로깅
log.error("Not Found Token");
throw new NullPointerException("No Found Token");
JWT 입력값 검증을 통화하지 못하였을 때,
오류 로그를 기록하고, 예외를 발생 시켜 잘못된 토큰 전달을 처리합니다.
(5) JWT 토큰 파싱 및 Claims를 추출하는 메소드
[토큰 파싱 및 Claims 추출하는 메소드 : extractClaims()]
public Claims extractClaims (String token){
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
🤔 JWT Claims 추출 메서드 사용 이유
JWT 토큰의 서명을 검증한 후,
토큰 내부에 포함된 사용자 정보(Claims)를 안전하게 추출하기 위해 사용됩니다.
이를 통해 애플리케이션은 사용자 인증 및 권한 부여와 관련된 정보를 확인할 수 있습니다.
✅ JWT 토큰 파싱 및 Claims 추출 과정
3. JwtUtil 전체 코드
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.example.statelessspringsecurity.enums.UserRole;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secrete.key}")
private String secreteKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init(){
byte[] bytes = Base64.getDecoder().decode(secreteKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createToken(Long userId, String email, UserRole userRole){
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email",email)
.claim("userRole",userRole.getUserRole())
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key,signatureAlgorithm)
.compact();
}
public String subStringToken (String tokenValue){
if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)){
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("No Found Token");
}
public Claims extractClaims (String token){
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}