update: common 파일 수정
- Audit Log 부분 수정 - jwt 부분 수정 - common에서 사용하는 열거형 추가
This commit is contained in:
parent
097cb0e7f8
commit
51d69c27e2
@ -9,5 +9,6 @@ public enum AuditAction {
|
||||
DELETE,
|
||||
ACCESS,
|
||||
LOGIN,
|
||||
LOGOUT
|
||||
LOGOUT,
|
||||
VIEW
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
// 유틸리티 클래스
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 로그아웃 로그
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user