update: common 파일 수정
- Audit Log 부분 수정 - jwt 부분 수정 - common에서 사용하는 열거형 추가
This commit is contained in:
parent
097cb0e7f8
commit
51d69c27e2
@ -9,5 +9,6 @@ public enum AuditAction {
|
|||||||
DELETE,
|
DELETE,
|
||||||
ACCESS,
|
ACCESS,
|
||||||
LOGIN,
|
LOGIN,
|
||||||
LOGOUT
|
LOGOUT,
|
||||||
|
VIEW
|
||||||
}
|
}
|
||||||
@ -1,9 +1,14 @@
|
|||||||
package com.ktds.hi.common.audit;
|
package com.ktds.hi.common.audit;
|
||||||
|
|
||||||
import com.ktds.hi.common.repository.AuditLogRepository;
|
import com.ktds.hi.common.repository.AuditLogRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -16,10 +21,14 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AuditLogger {
|
public class AuditLogger {
|
||||||
|
|
||||||
private final AuditLogRepository auditLogRepository;
|
private final AuditLogRepository auditLogRepository;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성 작업 감사 로그 기록
|
||||||
|
*/
|
||||||
public void logCreate(Object entity) {
|
public void logCreate(Object entity) {
|
||||||
|
try {
|
||||||
AuditLog auditLog = AuditLog.builder()
|
AuditLog auditLog = AuditLog.builder()
|
||||||
.entityType(entity.getClass().getSimpleName())
|
.entityType(entity.getClass().getSimpleName())
|
||||||
.entityId(extractEntityId(entity))
|
.entityId(extractEntityId(entity))
|
||||||
@ -31,26 +40,40 @@ public class AuditLogger {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
auditLogRepository.save(auditLog);
|
auditLogRepository.save(auditLog);
|
||||||
log.info("감사 로그 기록 - CREATE: {}", auditLog);
|
log.debug("감사 로그 기록 - CREATE: {}", auditLog);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to audit create operation", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정 작업 감사 로그 기록
|
||||||
|
*/
|
||||||
public void logUpdate(Object entity, Map<String, Object> oldValues, Map<String, Object> newValues) {
|
public void logUpdate(Object entity, Map<String, Object> oldValues, Map<String, Object> newValues) {
|
||||||
|
try {
|
||||||
AuditLog auditLog = AuditLog.builder()
|
AuditLog auditLog = AuditLog.builder()
|
||||||
.entityType(entity.getClass().getSimpleName())
|
.entityType(entity.getClass().getSimpleName())
|
||||||
.entityId(extractEntityId(entity))
|
.entityId(extractEntityId(entity))
|
||||||
.action(AuditAction.UPDATE)
|
.action(AuditAction.UPDATE)
|
||||||
.oldValues(oldValues.toString())
|
.oldValues(oldValues != null ? oldValues.toString() : null)
|
||||||
.newValues(newValues.toString())
|
.newValues(newValues != null ? newValues.toString() : null)
|
||||||
.userId(getCurrentUserInfo())
|
.userId(getCurrentUserInfo())
|
||||||
.ipAddress(getClientInfo())
|
.ipAddress(getClientInfo())
|
||||||
.timestamp(LocalDateTime.now())
|
.timestamp(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
auditLogRepository.save(auditLog);
|
auditLogRepository.save(auditLog);
|
||||||
log.info("감사 로그 기록 - UPDATE: {}", auditLog);
|
log.debug("감사 로그 기록 - UPDATE: {}", auditLog);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to audit update operation", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 삭제 작업 감사 로그 기록
|
||||||
|
*/
|
||||||
public void logDelete(Object entity) {
|
public void logDelete(Object entity) {
|
||||||
|
try {
|
||||||
AuditLog auditLog = AuditLog.builder()
|
AuditLog auditLog = AuditLog.builder()
|
||||||
.entityType(entity.getClass().getSimpleName())
|
.entityType(entity.getClass().getSimpleName())
|
||||||
.entityId(extractEntityId(entity))
|
.entityId(extractEntityId(entity))
|
||||||
@ -62,14 +85,21 @@ public class AuditLogger {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
auditLogRepository.save(auditLog);
|
auditLogRepository.save(auditLog);
|
||||||
log.info("감사 로그 기록 - DELETE: {}", auditLog);
|
log.debug("감사 로그 기록 - DELETE: {}", auditLog);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to audit delete operation", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 접근 작업 감사 로그 기록
|
||||||
|
*/
|
||||||
public void logAccess(String entityType, String entityId) {
|
public void logAccess(String entityType, String entityId) {
|
||||||
|
try {
|
||||||
AuditLog auditLog = AuditLog.builder()
|
AuditLog auditLog = AuditLog.builder()
|
||||||
.entityType(entityType)
|
.entityType(entityType)
|
||||||
.entityId(entityId)
|
.entityId(entityId)
|
||||||
.action(AuditAction.ACCESS)
|
.action(AuditAction.VIEW)
|
||||||
.userId(getCurrentUserInfo())
|
.userId(getCurrentUserInfo())
|
||||||
.ipAddress(getClientInfo())
|
.ipAddress(getClientInfo())
|
||||||
.timestamp(LocalDateTime.now())
|
.timestamp(LocalDateTime.now())
|
||||||
@ -77,29 +107,106 @@ public class AuditLogger {
|
|||||||
|
|
||||||
auditLogRepository.save(auditLog);
|
auditLogRepository.save(auditLog);
|
||||||
log.debug("감사 로그 기록 - ACCESS: {}", auditLog);
|
log.debug("감사 로그 기록 - ACCESS: {}", auditLog);
|
||||||
}
|
|
||||||
|
|
||||||
private String extractEntityId(Object entity) {
|
|
||||||
// 리플렉션을 사용하여 ID 필드 추출
|
|
||||||
try {
|
|
||||||
return entity.getClass().getMethod("getId").invoke(entity).toString();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return "unknown";
|
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) {
|
private String extractEntityInfo(Object entity) {
|
||||||
// 엔티티 정보를 JSON 형태로 변환
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 간단한 toString() 사용 (필요시 JSON 변환 로직으로 교체 가능)
|
||||||
return entity.toString();
|
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() {
|
private String getCurrentUserInfo() {
|
||||||
// 현재 사용자 정보 반환
|
try {
|
||||||
return "system"; // 실제 구현에서는 SecurityContext에서 가져옴
|
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() {
|
private String getClientInfo() {
|
||||||
// 클라이언트 IP 정보 반환
|
try {
|
||||||
return "127.0.0.1"; // 실제 구현에서는 HttpServletRequest에서 가져옴
|
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 {
|
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 AUTHORIZATION_HEADER = "Authorization";
|
||||||
public static final String BEARER_PREFIX = "Bearer ";
|
public static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
// 토큰 타입
|
||||||
public static final String TOKEN_TYPE_ACCESS = "access";
|
public static final String TOKEN_TYPE_ACCESS = "access";
|
||||||
public static final String TOKEN_TYPE_REFRESH = "refresh";
|
public static final String TOKEN_TYPE_REFRESH = "refresh";
|
||||||
|
|
||||||
|
// 토큰 만료 시간
|
||||||
public static final long ACCESS_TOKEN_EXPIRE_TIME = 30 * 60 * 1000L; // 30분
|
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일
|
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() {
|
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;
|
package com.ktds.hi.common.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 응답 래퍼
|
* 공통 API 응답 클래스
|
||||||
|
* 모든 API 응답에 사용되는 표준 형식
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class ApiResponse<T> {
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
private Boolean success;
|
private boolean success;
|
||||||
private String code;
|
private String code;
|
||||||
private String message;
|
private String message;
|
||||||
private T data;
|
private T data;
|
||||||
private Long timestamp;
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공 응답 생성
|
||||||
|
*/
|
||||||
public static <T> ApiResponse<T> success(T data) {
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
return ApiResponse.<T>builder()
|
return ApiResponse.<T>builder()
|
||||||
.success(true)
|
.success(true)
|
||||||
.code(ResponseCode.SUCCESS.getCode())
|
.code(ResponseCode.SUCCESS.getCode())
|
||||||
.message(ResponseCode.SUCCESS.getMessage())
|
.message(ResponseCode.SUCCESS.getMessage())
|
||||||
.data(data)
|
.data(data)
|
||||||
.timestamp(System.currentTimeMillis())
|
.timestamp(LocalDateTime.now())
|
||||||
.build();
|
.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()
|
return ApiResponse.<T>builder()
|
||||||
.success(true)
|
.success(true)
|
||||||
.code(ResponseCode.SUCCESS.getCode())
|
.code(ResponseCode.SUCCESS.getCode())
|
||||||
.message(message)
|
.message(message)
|
||||||
.data(data)
|
.data(data)
|
||||||
.timestamp(System.currentTimeMillis())
|
.timestamp(LocalDateTime.now())
|
||||||
.build();
|
.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) {
|
public static <T> ApiResponse<T> error(ResponseCode responseCode) {
|
||||||
return ApiResponse.<T>builder()
|
return ApiResponse.<T>builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.code(responseCode.getCode())
|
.code(responseCode.getCode())
|
||||||
.message(responseCode.getMessage())
|
.message(responseCode.getMessage())
|
||||||
.timestamp(System.currentTimeMillis())
|
.timestamp(LocalDateTime.now())
|
||||||
.build();
|
.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) {
|
public static <T> ApiResponse<T> error(String code, String message) {
|
||||||
return ApiResponse.<T>builder()
|
return ApiResponse.<T>builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.code(code)
|
.code(code)
|
||||||
.message(message)
|
.message(message)
|
||||||
.timestamp(System.currentTimeMillis())
|
.timestamp(LocalDateTime.now())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,29 +1,56 @@
|
|||||||
package com.ktds.hi.common.response;
|
package com.ktds.hi.common.response;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 응답 코드 열거형
|
* API 응답 코드 열거형
|
||||||
|
* 표준화된 응답 코드와 메시지 관리
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum ResponseCode {
|
public enum ResponseCode {
|
||||||
|
|
||||||
// 성공
|
// 성공
|
||||||
SUCCESS("200", "성공"),
|
SUCCESS("200", "성공"),
|
||||||
|
CREATED("201", "생성됨"),
|
||||||
|
|
||||||
// 클라이언트 에러
|
// 클라이언트 오류 (4xx)
|
||||||
BAD_REQUEST("400", "잘못된 요청"),
|
BAD_REQUEST("400", "잘못된 요청"),
|
||||||
UNAUTHORIZED("401", "인증 실패"),
|
UNAUTHORIZED("401", "인증 실패"),
|
||||||
FORBIDDEN("403", "접근 권한 없음"),
|
FORBIDDEN("403", "접근 권한 없음"),
|
||||||
NOT_FOUND("404", "리소스를 찾을 수 없음"),
|
NOT_FOUND("404", "리소스를 찾을 수 없음"),
|
||||||
|
METHOD_NOT_ALLOWED("405", "허용되지 않은 메서드"),
|
||||||
CONFLICT("409", "리소스 충돌"),
|
CONFLICT("409", "리소스 충돌"),
|
||||||
VALIDATION_ERROR("422", "입력값 검증 실패"),
|
UNPROCESSABLE_ENTITY("422", "처리할 수 없는 엔티티"),
|
||||||
|
|
||||||
// 서버 에러
|
// 서버 오류 (5xx)
|
||||||
INTERNAL_SERVER_ERROR("500", "내부 서버 오류"),
|
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 code;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.ktds.hi.common.security;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.ktds.hi.common.constants.SecurityConstants;
|
import com.ktds.hi.common.constants.SecurityConstants;
|
||||||
|
import com.ktds.hi.common.response.ApiResponse;
|
||||||
import com.ktds.hi.common.response.ResponseCode;
|
import com.ktds.hi.common.response.ResponseCode;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
|||||||
@ -5,13 +5,18 @@ import io.jsonwebtoken.*;
|
|||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +30,7 @@ public class JwtTokenProvider {
|
|||||||
private final SecretKey secretKey;
|
private final SecretKey secretKey;
|
||||||
private final long accessTokenValidityInMilliseconds;
|
private final long accessTokenValidityInMilliseconds;
|
||||||
private final long refreshTokenValidityInMilliseconds;
|
private final long refreshTokenValidityInMilliseconds;
|
||||||
|
private final JwtParser jwtParser;
|
||||||
|
|
||||||
public JwtTokenProvider(
|
public JwtTokenProvider(
|
||||||
@Value("${app.jwt.secret-key:hiorder-secret-key-for-jwt-token-generation-2024}") String secretKeyString,
|
@Value("${app.jwt.secret-key:hiorder-secret-key-for-jwt-token-generation-2024}") String secretKeyString,
|
||||||
@ -43,58 +49,43 @@ public class JwtTokenProvider {
|
|||||||
|
|
||||||
this.accessTokenValidityInMilliseconds = accessTokenValidity;
|
this.accessTokenValidityInMilliseconds = accessTokenValidity;
|
||||||
this.refreshTokenValidityInMilliseconds = refreshTokenValidity;
|
this.refreshTokenValidityInMilliseconds = refreshTokenValidity;
|
||||||
|
|
||||||
|
// JwtParser 초기화 (deprecated 메서드 대신 새로운 방식 사용)
|
||||||
|
this.jwtParser = Jwts.parser()
|
||||||
|
.verifyWith(secretKey)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 액세스 토큰 생성
|
* 액세스 토큰 생성
|
||||||
*/
|
*/
|
||||||
public String createAccessToken(Authentication authentication) {
|
public String createAccessToken(String userId, Collection<String> roles) {
|
||||||
return createToken(authentication, accessTokenValidityInMilliseconds, "access");
|
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) {
|
public String createRefreshToken(String userId) {
|
||||||
return createToken(authentication, refreshTokenValidityInMilliseconds, "refresh");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 사용자 정보로 액세스 토큰 생성
|
|
||||||
*/
|
|
||||||
public String createAccessToken(String userId, String username, String roles) {
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);
|
Date expiryDate = new Date(now.getTime() + refreshTokenValidityInMilliseconds);
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setSubject(userId)
|
.subject(userId)
|
||||||
.claim("username", username)
|
.claim("type", SecurityConstants.TOKEN_TYPE_REFRESH)
|
||||||
.claim("roles", roles)
|
.issuedAt(now)
|
||||||
.claim("type", "access")
|
.expiration(expiryDate)
|
||||||
.setIssuedAt(now)
|
.signWith(secretKey)
|
||||||
.setExpiration(expiryDate)
|
|
||||||
.signWith(secretKey, SignatureAlgorithm.HS512)
|
|
||||||
.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();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,32 +93,49 @@ public class JwtTokenProvider {
|
|||||||
* 토큰에서 사용자 ID 추출
|
* 토큰에서 사용자 ID 추출
|
||||||
*/
|
*/
|
||||||
public String getUserIdFromToken(String token) {
|
public String getUserIdFromToken(String token) {
|
||||||
Claims claims = parseClaimsFromToken(token);
|
try {
|
||||||
|
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
|
||||||
return claims.getSubject();
|
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) {
|
public String getRolesFromToken(String token) {
|
||||||
Claims claims = parseClaimsFromToken(token);
|
try {
|
||||||
|
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
|
||||||
return claims.get("roles", String.class);
|
return claims.get("roles", String.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("토큰에서 역할 정보 추출 실패", e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰에서 만료일 추출
|
* 토큰에서 인증 객체 생성
|
||||||
*/
|
*/
|
||||||
public Date getExpirationDateFromToken(String token) {
|
public Authentication getAuthentication(String token) {
|
||||||
Claims claims = parseClaimsFromToken(token);
|
try {
|
||||||
return claims.getExpiration();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,12 +143,22 @@ public class JwtTokenProvider {
|
|||||||
*/
|
*/
|
||||||
public boolean validateToken(String token) {
|
public boolean validateToken(String token) {
|
||||||
try {
|
try {
|
||||||
parseClaimsFromToken(token);
|
jwtParser.parseSignedClaims(token);
|
||||||
return true;
|
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());
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -148,9 +166,11 @@ public class JwtTokenProvider {
|
|||||||
*/
|
*/
|
||||||
public boolean isAccessToken(String token) {
|
public boolean isAccessToken(String token) {
|
||||||
try {
|
try {
|
||||||
Claims claims = parseClaimsFromToken(token);
|
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
|
||||||
return "access".equals(claims.get("type", String.class));
|
String tokenType = claims.get("type", String.class);
|
||||||
} catch (JwtException | IllegalArgumentException e) {
|
return SecurityConstants.TOKEN_TYPE_ACCESS.equals(tokenType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("토큰 타입 확인 실패", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,45 +180,46 @@ public class JwtTokenProvider {
|
|||||||
*/
|
*/
|
||||||
public boolean isRefreshToken(String token) {
|
public boolean isRefreshToken(String token) {
|
||||||
try {
|
try {
|
||||||
Claims claims = parseClaimsFromToken(token);
|
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
|
||||||
return "refresh".equals(claims.get("type", String.class));
|
String tokenType = claims.get("type", String.class);
|
||||||
} catch (JwtException | IllegalArgumentException e) {
|
return SecurityConstants.TOKEN_TYPE_REFRESH.equals(tokenType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("토큰 타입 확인 실패", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 만료 확인
|
* 토큰 만료 시간 가져오기
|
||||||
*/
|
*/
|
||||||
public boolean isTokenExpired(String token) {
|
public Date getExpirationDateFromToken(String token) {
|
||||||
try {
|
try {
|
||||||
Date expiration = getExpirationDateFromToken(token);
|
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
|
||||||
return expiration.before(new Date());
|
return claims.getExpiration();
|
||||||
} catch (JwtException | IllegalArgumentException e) {
|
} 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;
|
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.AuditAction;
|
||||||
import com.ktds.hi.common.audit.AuditLog;
|
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.repository.AuditLogRepository;
|
||||||
import com.ktds.hi.common.security.SecurityUtil;
|
import com.ktds.hi.common.security.SecurityUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -39,7 +40,7 @@ public class AuditLogService {
|
|||||||
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
|
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
|
||||||
String username = SecurityUtil.getCurrentUsername().orElse("SYSTEM");
|
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);
|
auditLogRepository.save(auditLog);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user