update: common 파일 수정

- Audit Log 부분 수정
- jwt 부분 수정
- common에서 사용하는 열거형 추가
This commit is contained in:
UNGGU0704 2025-06-12 14:46:20 +09:00
parent 097cb0e7f8
commit 51d69c27e2
9 changed files with 463 additions and 208 deletions

View File

@ -9,5 +9,6 @@ public enum AuditAction {
DELETE,
ACCESS,
LOGIN,
LOGOUT
LOGOUT,
VIEW
}

View File

@ -1,9 +1,14 @@
package com.ktds.hi.common.audit;
import com.ktds.hi.common.repository.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.util.Map;
@ -16,90 +21,192 @@ import java.util.Map;
@RequiredArgsConstructor
@Slf4j
public class AuditLogger {
private final AuditLogRepository auditLogRepository;
/**
* 생성 작업 감사 로그 기록
*/
public void logCreate(Object entity) {
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.CREATE)
.newValues(extractEntityInfo(entity))
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.info("감사 로그 기록 - CREATE: {}", auditLog);
}
public void logUpdate(Object entity, Map<String, Object> oldValues, Map<String, Object> newValues) {
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.UPDATE)
.oldValues(oldValues.toString())
.newValues(newValues.toString())
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.info("감사 로그 기록 - UPDATE: {}", auditLog);
}
public void logDelete(Object entity) {
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.DELETE)
.oldValues(extractEntityInfo(entity))
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.info("감사 로그 기록 - DELETE: {}", auditLog);
}
public void logAccess(String entityType, String entityId) {
AuditLog auditLog = AuditLog.builder()
.entityType(entityType)
.entityId(entityId)
.action(AuditAction.ACCESS)
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.debug("감사 로그 기록 - ACCESS: {}", auditLog);
}
private String extractEntityId(Object entity) {
// 리플렉션을 사용하여 ID 필드 추출
try {
return entity.getClass().getMethod("getId").invoke(entity).toString();
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.CREATE)
.newValues(extractEntityInfo(entity))
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.debug("감사 로그 기록 - CREATE: {}", auditLog);
} catch (Exception e) {
return "unknown";
log.warn("Failed to audit create operation", e);
}
}
/**
* 수정 작업 감사 로그 기록
*/
public void logUpdate(Object entity, Map<String, Object> oldValues, Map<String, Object> newValues) {
try {
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.UPDATE)
.oldValues(oldValues != null ? oldValues.toString() : null)
.newValues(newValues != null ? newValues.toString() : null)
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.debug("감사 로그 기록 - UPDATE: {}", auditLog);
} catch (Exception e) {
log.warn("Failed to audit update operation", e);
}
}
/**
* 삭제 작업 감사 로그 기록
*/
public void logDelete(Object entity) {
try {
AuditLog auditLog = AuditLog.builder()
.entityType(entity.getClass().getSimpleName())
.entityId(extractEntityId(entity))
.action(AuditAction.DELETE)
.oldValues(extractEntityInfo(entity))
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.debug("감사 로그 기록 - DELETE: {}", auditLog);
} catch (Exception e) {
log.warn("Failed to audit delete operation", e);
}
}
/**
* 접근 작업 감사 로그 기록
*/
public void logAccess(String entityType, String entityId) {
try {
AuditLog auditLog = AuditLog.builder()
.entityType(entityType)
.entityId(entityId)
.action(AuditAction.VIEW)
.userId(getCurrentUserInfo())
.ipAddress(getClientInfo())
.timestamp(LocalDateTime.now())
.build();
auditLogRepository.save(auditLog);
log.debug("감사 로그 기록 - ACCESS: {}", auditLog);
} catch (Exception e) {
log.warn("Failed to audit access operation", e);
}
}
/**
* 정적 create 메서드 (AuditLogService와의 호환성을 위해 추가)
*/
public static AuditLog create(Long userId, String username, AuditAction action,
String entityType, String entityId, String description) {
return AuditLog.builder()
.entityType(entityType)
.entityId(entityId)
.action(action)
.newValues(description)
.userId(username)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 엔티티 ID 추출
*/
private String extractEntityId(Object entity) {
if (entity == null) {
return "UNKNOWN";
}
try {
// 리플렉션을 사용하여 getId() 메서드 호출
var method = entity.getClass().getMethod("getId");
Object id = method.invoke(entity);
return id != null ? id.toString() : "UNKNOWN";
} catch (Exception e) {
log.debug("Failed to extract entity ID from {}", entity.getClass().getSimpleName(), e);
return "UNKNOWN";
}
}
/**
* 엔티티 정보를 문자열로 변환
*/
private String extractEntityInfo(Object entity) {
// 엔티티 정보를 JSON 형태로 변환
return entity.toString();
if (entity == null) {
return null;
}
try {
// 간단한 toString() 사용 (필요시 JSON 변환 로직으로 교체 가능)
return entity.toString();
} catch (Exception e) {
log.debug("Failed to extract entity info from {}", entity.getClass().getSimpleName(), e);
return entity.getClass().getSimpleName() + "@" + System.identityHashCode(entity);
}
}
/**
* 현재 사용자 정보 반환
*/
private String getCurrentUserInfo() {
// 현재 사용자 정보 반환
return "system"; // 실제 구현에서는 SecurityContext에서 가져옴
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()
&& !"anonymousUser".equals(authentication.getPrincipal())) {
return authentication.getName();
}
} catch (Exception e) {
log.debug("Failed to get current user info", e);
}
return "SYSTEM";
}
/**
* 클라이언트 정보 (IP 주소) 반환
*/
private String getClientInfo() {
// 클라이언트 IP 정보 반환
return "127.0.0.1"; // 실제 구현에서는 HttpServletRequest에서 가져옴
try {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// X-Forwarded-For 헤더 확인 (프록시/로드밸런서 환경)
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
// X-Real-IP 헤더 확인
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
// 기본 원격 주소
return request.getRemoteAddr();
} catch (Exception e) {
log.debug("Failed to get client info", e);
return "127.0.0.1";
}
}
}
}

View File

@ -5,14 +5,35 @@ package com.ktds.hi.common.constants;
*/
public class SecurityConstants {
// JWT 헤더 관련
public static final String JWT_HEADER = "Authorization";
public static final String JWT_PREFIX = "Bearer ";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 타입
public static final String TOKEN_TYPE_ACCESS = "access";
public static final String TOKEN_TYPE_REFRESH = "refresh";
// 토큰 만료 시간
public static final long ACCESS_TOKEN_EXPIRE_TIME = 30 * 60 * 1000L; // 30분
public static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; // 7일
// 공개 엔드포인트 (JWT 인증 제외)
public static final String[] PUBLIC_ENDPOINTS = {
"/api/auth/**",
"/api/members/register",
"/api/members/check-username/**",
"/api/members/check-nickname/**",
"/api/stores/search/**",
"/api/stores/*/reviews/**",
"/swagger-ui/**",
"/api-docs/**",
"/actuator/**",
"/error",
"/favicon.ico"
};
private SecurityConstants() {
// 유틸리티 클래스
}

View File

@ -0,0 +1,35 @@
package com.ktds.hi.common.eunms;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 감사 로그 액션 열거형
* 시스템에서 발생하는 모든 감사 대상 액션을 정의
*/
@Getter
@RequiredArgsConstructor
public enum AuditAction {
CREATE("생성", "새로운 데이터가 생성됨"),
READ("조회", "데이터가 조회됨"),
UPDATE("수정", "기존 데이터가 수정됨"),
DELETE("삭제", "데이터가 삭제됨"),
LOGIN("로그인", "사용자가 로그인함"),
LOGOUT("로그아웃", "사용자가 로그아웃함"),
REGISTER("회원가입", "새로운 사용자가 회원가입함"),
PASSWORD_CHANGE("비밀번호 변경", "사용자가 비밀번호를 변경함"),
PROFILE_UPDATE("프로필 수정", "사용자가 프로필을 수정함"),
FILE_UPLOAD("파일 업로드", "파일이 업로드됨"),
FILE_DOWNLOAD("파일 다운로드", "파일이 다운로드됨"),
PERMISSION_GRANT("권한 부여", "사용자에게 권한이 부여됨"),
PERMISSION_REVOKE("권한 회수", "사용자의 권한이 회수됨"),
DATA_EXPORT("데이터 내보내기", "데이터가 내보내기됨"),
DATA_IMPORT("데이터 가져오기", "데이터가 가져오기됨"),
SYSTEM_ACCESS("시스템 접근", "시스템에 접근함"),
API_CALL("API 호출", "API가 호출됨"),
ERROR("오류", "시스템 오류가 발생함");
private final String displayName;
private final String description;
}

View File

@ -1,60 +1,101 @@
package com.ktds.hi.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* API 응답 래퍼
* 공통 API 응답 클래스
* 모든 API 응답에 사용되는 표준 형식
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private Boolean success;
private boolean success;
private String code;
private String message;
private T data;
private Long timestamp;
private LocalDateTime timestamp;
/**
* 성공 응답 생성
*/
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.code(ResponseCode.SUCCESS.getCode())
.message(ResponseCode.SUCCESS.getMessage())
.data(data)
.timestamp(System.currentTimeMillis())
.timestamp(LocalDateTime.now())
.build();
}
public static <T> ApiResponse<T> success(String message, T data) {
/**
* 성공 응답 생성 (메시지 포함)
*/
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.success(true)
.code(ResponseCode.SUCCESS.getCode())
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
.timestamp(LocalDateTime.now())
.build();
}
/**
* 성공 응답 생성 (데이터 없음)
*/
public static ApiResponse<Void> success() {
return ApiResponse.<Void>builder()
.success(true)
.code(ResponseCode.SUCCESS.getCode())
.message(ResponseCode.SUCCESS.getMessage())
.timestamp(LocalDateTime.now())
.build();
}
/**
* 에러 응답 생성
*/
public static <T> ApiResponse<T> error(ResponseCode responseCode) {
return ApiResponse.<T>builder()
.success(false)
.code(responseCode.getCode())
.message(responseCode.getMessage())
.timestamp(System.currentTimeMillis())
.timestamp(LocalDateTime.now())
.build();
}
/**
* 에러 응답 생성 (메시지 포함)
*/
public static <T> ApiResponse<T> error(ResponseCode responseCode, String message) {
return ApiResponse.<T>builder()
.success(false)
.code(responseCode.getCode())
.message(message)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 에러 응답 생성 (코드와 메시지 직접 지정)
*/
public static <T> ApiResponse<T> error(String code, String message) {
return ApiResponse.<T>builder()
.success(false)
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.timestamp(LocalDateTime.now())
.build();
}
}

View File

@ -1,29 +1,56 @@
package com.ktds.hi.common.response;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 응답 코드 열거형
* API 응답 코드 열거형
* 표준화된 응답 코드와 메시지 관리
*/
@Getter
@AllArgsConstructor
@RequiredArgsConstructor
public enum ResponseCode {
// 성공
SUCCESS("200", "성공"),
CREATED("201", "생성됨"),
// 클라이언트 에러
// 클라이언트 오류 (4xx)
BAD_REQUEST("400", "잘못된 요청"),
UNAUTHORIZED("401", "인증 실패"),
FORBIDDEN("403", "접근 권한 없음"),
NOT_FOUND("404", "리소스를 찾을 수 없음"),
METHOD_NOT_ALLOWED("405", "허용되지 않은 메서드"),
CONFLICT("409", "리소스 충돌"),
VALIDATION_ERROR("422", "입력값 검증 실패"),
UNPROCESSABLE_ENTITY("422", "처리할 수 없는 엔티티"),
// 서버 에러
// 서버 오류 (5xx)
INTERNAL_SERVER_ERROR("500", "내부 서버 오류"),
SERVICE_UNAVAILABLE("503", "서비스 이용 불가");
BAD_GATEWAY("502", "잘못된 게이트웨이"),
SERVICE_UNAVAILABLE("503", "서비스 사용 불가"),
// 비즈니스 로직 오류
INVALID_INPUT("1001", "입력값이 올바르지 않습니다"),
DUPLICATE_USERNAME("1002", "이미 사용중인 아이디입니다"),
DUPLICATE_NICKNAME("1003", "이미 사용중인 닉네임입니다"),
INVALID_CREDENTIALS("1004", "아이디 또는 비밀번호가 일치하지 않습니다"),
ACCESS_DENIED("1005", "접근 권한이 없습니다"),
EXPIRED_TOKEN("1006", "토큰이 만료되었습니다"),
INVALID_TOKEN("1007", "유효하지 않은 토큰입니다"),
USER_NOT_FOUND("1008", "사용자를 찾을 수 없습니다"),
STORE_NOT_FOUND("1009", "매장을 찾을 수 없습니다"),
REVIEW_NOT_FOUND("1010", "리뷰를 찾을 수 없습니다"),
// 파일 관련 오류
FILE_UPLOAD_ERROR("2001", "파일 업로드 실패"),
FILE_NOT_FOUND("2002", "파일을 찾을 수 없습니다"),
INVALID_FILE_FORMAT("2003", "지원하지 않는 파일 형식입니다"),
FILE_SIZE_EXCEEDED("2004", "파일 크기가 허용 범위를 초과했습니다"),
// 외부 서비스 오류
SMS_SEND_ERROR("3001", "SMS 전송 실패"),
EMAIL_SEND_ERROR("3002", "이메일 전송 실패"),
EXTERNAL_API_ERROR("3003", "외부 API 호출 실패");
private final String code;
private final String message;

View File

@ -2,6 +2,7 @@ package com.ktds.hi.common.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ktds.hi.common.constants.SecurityConstants;
import com.ktds.hi.common.response.ApiResponse;
import com.ktds.hi.common.response.ResponseCode;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;

View File

@ -5,13 +5,18 @@ import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
@ -21,16 +26,17 @@ import java.util.stream.Collectors;
@Component
@Slf4j
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long accessTokenValidityInMilliseconds;
private final long refreshTokenValidityInMilliseconds;
private final JwtParser jwtParser;
public JwtTokenProvider(
@Value("${app.jwt.secret-key:hiorder-secret-key-for-jwt-token-generation-2024}") String secretKeyString,
@Value("${app.jwt.access-token-validity:3600000}") long accessTokenValidity,
@Value("${app.jwt.refresh-token-validity:604800000}") long refreshTokenValidity) {
// 비밀키 생성 (256비트 이상이어야 )
byte[] keyBytes = secretKeyString.getBytes(StandardCharsets.UTF_8);
if (keyBytes.length < 32) {
@ -40,165 +46,180 @@ public class JwtTokenProvider {
keyBytes = paddedKey;
}
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
this.accessTokenValidityInMilliseconds = accessTokenValidity;
this.refreshTokenValidityInMilliseconds = refreshTokenValidity;
// JwtParser 초기화 (deprecated 메서드 대신 새로운 방식 사용)
this.jwtParser = Jwts.parser()
.verifyWith(secretKey)
.build();
}
/**
* 액세스 토큰 생성
*/
public String createAccessToken(Authentication authentication) {
return createToken(authentication, accessTokenValidityInMilliseconds, "access");
public String createAccessToken(String userId, Collection<String> roles) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);
return Jwts.builder()
.subject(userId)
.claim("type", SecurityConstants.TOKEN_TYPE_ACCESS)
.claim("roles", String.join(",", roles))
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 리프레시 토큰 생성
*/
public String createRefreshToken(Authentication authentication) {
return createToken(authentication, refreshTokenValidityInMilliseconds, "refresh");
}
/**
* 사용자 정보로 액세스 토큰 생성
*/
public String createAccessToken(String userId, String username, String roles) {
public String createRefreshToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);
Date expiryDate = new Date(now.getTime() + refreshTokenValidityInMilliseconds);
return Jwts.builder()
.setSubject(userId)
.claim("username", username)
.claim("roles", roles)
.claim("type", "access")
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS512)
.subject(userId)
.claim("type", SecurityConstants.TOKEN_TYPE_REFRESH)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 토큰 생성 공통 메서드
*/
private String createToken(Authentication authentication, long validityInMilliseconds, String tokenType) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
Date now = new Date();
Date expiryDate = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName())
.claim("roles", authorities)
.claim("type", tokenType)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}
/**
* 토큰에서 사용자 ID 추출
*/
public String getUserIdFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.getSubject();
try {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
return claims.getSubject();
} catch (Exception e) {
log.error("토큰에서 사용자 ID 추출 실패", e);
return null;
}
}
/**
* 토큰에서 사용자명 추출
*/
public String getUsernameFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.get("username", String.class);
}
/**
* 토큰에서 권한 추출
* 토큰에서 역할 정보 추출
*/
public String getRolesFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.get("roles", String.class);
try {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
return claims.get("roles", String.class);
} catch (Exception e) {
log.error("토큰에서 역할 정보 추출 실패", e);
return "";
}
}
/**
* 토큰에서 만료일 추출
* 토큰에서 인증 객체 생성
*/
public Date getExpirationDateFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.getExpiration();
public Authentication getAuthentication(String token) {
try {
String userId = getUserIdFromToken(token);
String roles = getRolesFromToken(token);
if (userId != null) {
List<SimpleGrantedAuthority> authorities = Arrays.stream(roles.split(","))
.filter(role -> !role.trim().isEmpty())
.map(String::trim)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(userId, null, authorities);
}
} catch (Exception e) {
log.error("토큰에서 인증 객체 생성 실패", e);
}
return null;
}
/**
* 토큰 유효성 검증
*/
public boolean validateToken(String token) {
try {
parseClaimsFromToken(token);
jwtParser.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
} catch (SecurityException e) {
log.debug("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.debug("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.debug("Expired JWT token: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.debug("Unsupported JWT token: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.debug("JWT claims string is empty: {}", e.getMessage());
} catch (Exception e) {
log.debug("JWT token validation failed: {}", e.getMessage());
}
return false;
}
/**
* 액세스 토큰인지 확인
*/
public boolean isAccessToken(String token) {
try {
Claims claims = parseClaimsFromToken(token);
return "access".equals(claims.get("type", String.class));
} catch (JwtException | IllegalArgumentException e) {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
String tokenType = claims.get("type", String.class);
return SecurityConstants.TOKEN_TYPE_ACCESS.equals(tokenType);
} catch (Exception e) {
log.debug("토큰 타입 확인 실패", e);
return false;
}
}
/**
* 리프레시 토큰인지 확인
*/
public boolean isRefreshToken(String token) {
try {
Claims claims = parseClaimsFromToken(token);
return "refresh".equals(claims.get("type", String.class));
} catch (JwtException | IllegalArgumentException e) {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
String tokenType = claims.get("type", String.class);
return SecurityConstants.TOKEN_TYPE_REFRESH.equals(tokenType);
} catch (Exception e) {
log.debug("토큰 타입 확인 실패", e);
return false;
}
}
/**
* 토큰 만료 확인
* 토큰 만료 시간 가져오기
*/
public boolean isTokenExpired(String token) {
public Date getExpirationDateFromToken(String token) {
try {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (JwtException | IllegalArgumentException e) {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
return claims.getExpiration();
} catch (Exception e) {
log.error("토큰에서 만료 시간 추출 실패", e);
return null;
}
}
/**
* 토큰이 만료되는지 확인 (15분 이내)
*/
public boolean isTokenExpiringSoon(String token) {
try {
Date expirationDate = getExpirationDateFromToken(token);
if (expirationDate == null) {
return true;
}
long now = System.currentTimeMillis();
long expiration = expirationDate.getTime();
long fifteenMinutes = 15 * 60 * 1000; // 15분
return (expiration - now) < fifteenMinutes;
} catch (Exception e) {
log.debug("토큰 만료 임박 확인 실패", e);
return true;
}
}
/**
* 토큰에서 Claims 파싱
*/
private Claims parseClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 토큰 만료 시간까지 남은 시간 (밀리초)
*/
public long getTimeUntilExpiration(String token) {
try {
Date expiration = getExpirationDateFromToken(token);
return Math.max(0, expiration.getTime() - System.currentTimeMillis());
} catch (JwtException | IllegalArgumentException e) {
return 0;
}
}
}
}

View File

@ -2,6 +2,7 @@ package com.ktds.hi.common.service;
import com.ktds.hi.common.audit.AuditAction;
import com.ktds.hi.common.audit.AuditLog;
import com.ktds.hi.common.audit.AuditLogger;
import com.ktds.hi.common.repository.AuditLogRepository;
import com.ktds.hi.common.security.SecurityUtil;
import lombok.RequiredArgsConstructor;
@ -18,9 +19,9 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Slf4j
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
/**
* 감사 로그 기록 (비동기)
*/
@ -29,7 +30,7 @@ public class AuditLogService {
public void logAsync(AuditAction action, String entityType, String entityId, String description) {
log(action, entityType, entityId, description);
}
/**
* 감사 로그 기록 (동기)
*/
@ -38,51 +39,51 @@ public class AuditLogService {
try {
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
String username = SecurityUtil.getCurrentUsername().orElse("SYSTEM");
AuditLog auditLog = AuditLog.create(userId, username, action, entityType, entityId, description);
AuditLog auditLog = AuditLogger.create(userId, username, action, entityType, entityId, description);
auditLogRepository.save(auditLog);
} catch (Exception e) {
log.error("Failed to save audit log: action={}, entityType={}, entityId={}",
log.error("Failed to save audit log: action={}, entityType={}, entityId={}",
action, entityType, entityId, e);
}
}
/**
* 생성 로그
*/
public void logCreate(String entityType, String entityId, String description) {
logAsync(AuditAction.CREATE, entityType, entityId, description);
}
/**
* 수정 로그
*/
public void logUpdate(String entityType, String entityId, String description) {
logAsync(AuditAction.UPDATE, entityType, entityId, description);
}
/**
* 삭제 로그
*/
public void logDelete(String entityType, String entityId, String description) {
logAsync(AuditAction.DELETE, entityType, entityId, description);
}
/**
* 접근 로그
*/
public void logAccess(String entityType, String entityId, String description) {
logAsync(AuditAction.ACCESS, entityType, entityId, description);
}
/**
* 로그인 로그
*/
public void logLogin(String description) {
logAsync(AuditAction.LOGIN, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description);
}
/**
* 로그아웃 로그
*/