package com.takensoft.common.util; import com.takensoft.cms.mber.vo.MberAuthorVO; import com.takensoft.cms.mber.vo.MberVO; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.util.*; /** * @author : takensoft * @since : 2025.01.22 * @modification * since | author | description * 2025.01.22 | takensoft | 최초 등록 * 2025.06.18 | takensoft | 토큰 추출 로직 통합 및 중복 제거 * * JWT 토큰 생성 및 검증, 쿠키 생성 등의 유틸리티 - 토큰 추출 로직 통합 */ @Component public class JWTUtil { private static SecretKey JWT_SECRET_KEY; // 토큰 추출 우선순위 정의 private static final String[] TOKEN_COOKIE_NAMES = {"Authorization", "refresh"}; private static final String BEARER_PREFIX = "Bearer "; private static final String AUTHORIZATION_HEADER = "Authorization"; /** * @param secret - JWT 서명을 위한 키 (application.yml에서 값을 읽어 옴) * * 기본 생성자 */ public JWTUtil(@Value("${jwt.secret}")String secret) { this.JWT_SECRET_KEY = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); } /** * @param request - HTTP 요청 객체 * @return 추출된 JWT 토큰 (Bearer 접두사 제거된 순수 토큰) 또는 null * * HTTP 요청에서 JWT 토큰을 추출하는 통합 메서드 * 우선순위: Authorization 헤더 → Authorization 쿠키 → refresh 쿠키 */ public String extractTokenFromRequest(HttpServletRequest request) { if (request == null) { return null; } // 1. Authorization 헤더에서 토큰 추출 시도 String authHeader = request.getHeader(AUTHORIZATION_HEADER); if (isValidTokenString(authHeader)) { return removeBearerPrefix(authHeader); } // 2. 쿠키에서 토큰 추출 시도 String tokenFromCookie = extractTokenFromCookie(request); if (isValidTokenString(tokenFromCookie)) { return removeBearerPrefix(tokenFromCookie); } return null; } /** * @param authHeader - Authorization 헤더 문자열 또는 토큰 문자열 * @return Bearer 접두사가 제거된 순수 토큰 문자열 * * Bearer 접두사를 제거하는 기존 메서드 (하위 호환성 유지) */ public String extractToken(String authHeader) { return removeBearerPrefix(authHeader); } /** * @param request - HTTP 요청 객체 * @return 쿠키에서 추출된 토큰 문자열 또는 null * * 쿠키에서 토큰을 추출하는 전용 메서드 * 우선순위: Authorization 쿠키 → refresh 쿠키 */ private String extractTokenFromCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return null; } // 쿠키 우선순위에 따라 토큰 추출 for (String cookieName : TOKEN_COOKIE_NAMES) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { String cookieValue = cookie.getValue(); if (isValidTokenString(cookieValue)) { return cookieValue; } } } } return null; } /** * @param tokenString - 검증할 토큰 문자열 * @return 유효한 토큰 문자열인지 여부 * * 토큰 문자열 유효성 검증 */ private boolean isValidTokenString(String tokenString) { return tokenString != null && !tokenString.trim().isEmpty() && !tokenString.equalsIgnoreCase("null") && !tokenString.equalsIgnoreCase("undefined"); } /** * @param authHeader - Bearer 접두사가 포함될 수 있는 토큰 문자열 * @return Bearer 접두사가 제거된 순수 토큰 문자열 * * Bearer 접두사 제거 로직 */ private String removeBearerPrefix(String authHeader) { if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) { return authHeader.substring(BEARER_PREFIX.length()); } return authHeader; } /** * @param category 토큰의 카테고리 정보 (Authorization, refresh) * @param mbrId 사용자 ID * @param lgnId 로그인 ID * @param mbrNm 사용자 이름 * @param roles 사용자 권한 목록 * @param expiredMs 토큰 만료 시간 (밀리초) * @return 생성된 JWT 토큰 (String) * * JWT 토큰 생성 */ public String createJwt(String category, String mbrId, String lgnId, String mbrNm, List roles, long expiredMs) { return Jwts.builder() .claim("category", category) .claim("mbrId", mbrId) .claim("lgnId", lgnId) .claim("mbrNm", mbrNm) .claim("roles", roles) .issuedAt(new Date(System.currentTimeMillis())) // 토큰 발행 시간 .expiration(new Date(System.currentTimeMillis() + expiredMs)) // 토큰 소멸 시간 .signWith(JWT_SECRET_KEY) .compact(); } /** * @param key 쿠키 키 값 * @param value 쿠키 값 * @param time 쿠키의 생명주기 (초 단위) * @return 생성된 Cookie 객체 * * 쿠키 생성 */ public Cookie createCookie(String key, String value, int time) { // 쿠키 생성 Cookie cookie = new Cookie(key, value); cookie.setMaxAge(time); // 생명주기 //cookie.setSecure(true); // https 통신을 할 경우 true로 사용 cookie.setPath("/"); // 쿠키 적용 범위 cookie.setHttpOnly(true); // front에서 script로 접근 방지 return cookie; } /** * @param tkn JWT 토큰 문자열 * @param knd 조회할 데이터의 종류 (예: ctgry, userId, lgnId 등) * @return 조회된 클레임 데이터 (종류에 따라 String, Date, List 등으로 반환) * @throws IllegalArgumentException 유효하지 않은 knd 값일 경우 예외 발생 * * 클레임 조회 */ public Object getClaim(String tkn, String knd) { Claims claims; try { // 토큰 값 검증 if (tkn == null || tkn.trim().isEmpty()) { throw new IllegalArgumentException("Token is null or empty"); } else { tkn = extractToken(tkn); } claims = Jwts.parser() .verifyWith(JWT_SECRET_KEY) .build() .parseSignedClaims(tkn) .getPayload(); } catch (ExpiredJwtException e) { // 만료된 토큰이라도 claims 꺼내기 가능 claims = e.getClaims(); } catch (JwtException | IllegalArgumentException e) { // 토큰 자체가 잘못된 경우 throw new IllegalArgumentException("Invalid token"); } switch (knd) { case "category": return claims.get("category", String.class); case "mbrId": return claims.get("mbrId", String.class); case "lgnId": return claims.get("lgnId", String.class); case "mbrNm": return claims.get("mbrNm", String.class); case "roles": List> roles = claims.get("roles", List.class); List authorList = new ArrayList<>(); if (roles != null && !roles.isEmpty()) { for (Map role : roles) { MberAuthorVO userAuthor = new MberAuthorVO(role.get("authority").toString()); authorList.add(userAuthor); } } return authorList; case "isExpired": return claims.getExpiration().before(new Date()); case "expired": return claims.getExpiration(); default: throw new IllegalArgumentException("Invalid knd : " + knd); } } }