package com.zioinfo.mall.auth; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; @Slf4j @Component public class JwtUtil { @Value("${guardia.jwt.secret:guardia-mall-jwt-secret-2026-minimum-256bit-key-zioinfo}") private String secret; @Value("${guardia.jwt.expiration:86400000}") private long expirationMs; private SecretKey key() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generate(String username, String role) { return Jwts.builder() .subject(username) .claim("role", role) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expirationMs)) .signWith(key()) .compact(); } /** * UIWS 2FA 이식: 1차 로그인 통과 후 발급하는 단기 verify-token. * purpose=2fa 클레임으로 access 토큰과 구분(verify-token 으로는 보호 API 접근 불가). */ public String generateVerifyToken(String username, long validitySeconds) { return Jwts.builder() .subject(username) .claim("purpose", "2fa") .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + validitySeconds * 1000L)) .signWith(key()) .compact(); } /** verify-token 검증 후 username 반환. 유효하지 않거나 purpose!=2fa 면 null. */ public String parseVerifyTokenUsername(String token) { try { Claims c = parse(token); if (!"2fa".equals(c.get("purpose", String.class))) { return null; } return c.getSubject(); } catch (JwtException | IllegalArgumentException e) { log.debug("verify-token 검증 실패: {}", e.getMessage()); return null; } } /** * 보안: 토큰이 2FA verify-token(purpose=2fa)인지 판별. * JwtFilter 가 access 토큰만 인증 컨텍스트로 인정하도록 verify-token 을 걸러내는 데 사용. * verify-token 은 access 와 동일 서명키라 isValid() 는 통과 → 반드시 별도 차단(2FA 우회 방지). */ public boolean isVerifyToken(String token) { try { return "2fa".equals(parse(token).get("purpose", String.class)); } catch (JwtException | IllegalArgumentException e) { return false; } } public Claims parse(String token) { return Jwts.parser().verifyWith(key()).build() .parseSignedClaims(token).getPayload(); } public boolean isValid(String token) { try { parse(token); return true; } catch (JwtException | IllegalArgumentException e) { log.debug("JWT 검증 실패: {}", e.getMessage()); return false; } } public String getUsername(String token) { return parse(token).getSubject(); } public String getRole(String token) { return parse(token).get("role", String.class); } }