init
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package com.ktds.hi.common;
|
||||
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* Common 모듈 설정 클래스
|
||||
* 다른 모듈에서 Common 모듈을 사용할 때 필요한 설정을 자동으로 적용
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "com.ktds.hi.common")
|
||||
@EntityScan(basePackages = "com.ktds.hi.common.entity")
|
||||
@EnableJpaRepositories(basePackages = "com.ktds.hi.common.repository")
|
||||
public class CommonModuleConfiguration {
|
||||
// 설정 클래스는 어노테이션만으로도 충분
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ktds.hi.common.aspect;
|
||||
|
||||
import com.ktds.hi.common.service.AuditLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 감사 로깅 AOP
|
||||
* 특정 메서드 실행 시 자동으로 감사 로그를 기록
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuditAspect {
|
||||
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
/**
|
||||
* 서비스 메서드 실행 후 감사 로그 기록
|
||||
*/
|
||||
@AfterReturning(
|
||||
pointcut = "execution(* com.ktds.hi.*.biz.service.*Service.create*(..))",
|
||||
returning = "result"
|
||||
)
|
||||
public void auditCreate(JoinPoint joinPoint, Object result) {
|
||||
try {
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
|
||||
auditLogService.logCreate(
|
||||
className.replace("Service", ""),
|
||||
extractEntityId(result),
|
||||
String.format("%s.%s 실행", className, methodName)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to audit create operation", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 객체에서 ID 추출
|
||||
*/
|
||||
private String extractEntityId(Object result) {
|
||||
if (result == null) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
try {
|
||||
// 리플렉션을 사용하여 getId() 메서드 호출
|
||||
var method = result.getClass().getMethod("getId");
|
||||
Object id = method.invoke(result);
|
||||
return id != null ? id.toString() : "UNKNOWN";
|
||||
} catch (Exception e) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ktds.hi.common.repository;
|
||||
|
||||
import com.ktds.hi.common.audit.AuditAction;
|
||||
import com.ktds.hi.common.audit.AuditLog;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 감사 로그 리포지토리
|
||||
*/
|
||||
@Repository
|
||||
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
|
||||
|
||||
/**
|
||||
* 사용자별 감사 로그 조회
|
||||
*/
|
||||
Page<AuditLog> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 액션별 감사 로그 조회
|
||||
*/
|
||||
Page<AuditLog> findByActionOrderByCreatedAtDesc(AuditAction action, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 엔티티별 감사 로그 조회
|
||||
*/
|
||||
Page<AuditLog> findByEntityTypeAndEntityIdOrderByCreatedAtDesc(String entityType, String entityId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 기간별 감사 로그 조회
|
||||
*/
|
||||
@Query("SELECT al FROM AuditLog al WHERE al.createdAt BETWEEN :startDate AND :endDate ORDER BY al.createdAt DESC")
|
||||
Page<AuditLog> findByCreatedAtBetween(@Param("startDate") LocalDateTime startDate,
|
||||
@Param("endDate") LocalDateTime endDate,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 사용자 액션 통계 조회
|
||||
*/
|
||||
@Query("SELECT al.action, COUNT(al) FROM AuditLog al WHERE al.userId = :userId GROUP BY al.action")
|
||||
List<Object[]> findActionStatsByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 일별 로그 수 조회
|
||||
*/
|
||||
@Query("SELECT DATE(al.createdAt), COUNT(al) FROM AuditLog al WHERE al.createdAt >= :startDate GROUP BY DATE(al.createdAt) ORDER BY DATE(al.createdAt)")
|
||||
List<Object[]> findDailyLogCounts(@Param("startDate") LocalDateTime startDate);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ktds.hi.common.security;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 커스텀 사용자 상세 정보
|
||||
* Spring Security UserDetails 인터페이스 구현
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final Long id;
|
||||
private final String username;
|
||||
private final String email;
|
||||
private final String password;
|
||||
private final List<String> roles;
|
||||
private final boolean enabled;
|
||||
private final boolean accountNonExpired;
|
||||
private final boolean accountNonLocked;
|
||||
private final boolean credentialsNonExpired;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return roles.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return accountNonExpired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return accountNonLocked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return credentialsNonExpired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 생성자 (활성화된 사용자)
|
||||
*/
|
||||
public static CustomUserDetails of(Long id, String username, String email, String password, List<String> roles) {
|
||||
return new CustomUserDetails(
|
||||
id, username, email, password, roles,
|
||||
true, true, true, true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태를 지정한 생성자
|
||||
*/
|
||||
public static CustomUserDetails of(Long id, String username, String email, String password, List<String> roles,
|
||||
boolean enabled, boolean accountNonExpired,
|
||||
boolean accountNonLocked, boolean credentialsNonExpired) {
|
||||
return new CustomUserDetails(
|
||||
id, username, email, password, roles,
|
||||
enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT 인증 필터
|
||||
* HTTP 요청에서 JWT 토큰을 추출하고 검증하여 인증 처리
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
String jwt = getJwtFromRequest(request);
|
||||
|
||||
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
|
||||
// 액세스 토큰인지 확인
|
||||
if (!jwtTokenProvider.isAccessToken(jwt)) {
|
||||
sendErrorResponse(response, ResponseCode.UNAUTHORIZED, "액세스 토큰이 아닙니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = jwtTokenProvider.getUserIdFromToken(jwt);
|
||||
String roles = jwtTokenProvider.getRolesFromToken(jwt);
|
||||
|
||||
if (StringUtils.hasText(userId)) {
|
||||
List<SimpleGrantedAuthority> authorities = Arrays.stream(roles.split(","))
|
||||
.map(String::trim)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userId, null, authorities);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT authentication failed", e);
|
||||
sendErrorResponse(response, ResponseCode.UNAUTHORIZED, "인증에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청에서 JWT 토큰 추출
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(SecurityConstants.JWT_HEADER);
|
||||
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(SecurityConstants.JWT_PREFIX)) {
|
||||
return bearerToken.substring(SecurityConstants.JWT_PREFIX.length());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 전송
|
||||
*/
|
||||
private void sendErrorResponse(HttpServletResponse response, ResponseCode responseCode, String message)
|
||||
throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
ApiResponse<Void> errorResponse = ApiResponse.error(responseCode, message);
|
||||
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 경로인지 확인
|
||||
*/
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||
String path = request.getRequestURI();
|
||||
|
||||
return Arrays.stream(SecurityConstants.PUBLIC_ENDPOINTS)
|
||||
.anyMatch(pattern -> path.matches(pattern.replace("**", ".*")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.ktds.hi.common.security;
|
||||
|
||||
import com.ktds.hi.common.constants.SecurityConstants;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT 토큰 생성 및 검증 제공자
|
||||
* JWT 토큰의 생성, 파싱, 검증 기능을 담당
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long accessTokenValidityInMilliseconds;
|
||||
private final long refreshTokenValidityInMilliseconds;
|
||||
|
||||
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) {
|
||||
// 32바이트 미만이면 패딩
|
||||
byte[] paddedKey = new byte[32];
|
||||
System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyBytes.length, 32));
|
||||
keyBytes = paddedKey;
|
||||
}
|
||||
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
|
||||
|
||||
this.accessTokenValidityInMilliseconds = accessTokenValidity;
|
||||
this.refreshTokenValidityInMilliseconds = refreshTokenValidity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰 생성
|
||||
*/
|
||||
public String createAccessToken(Authentication authentication) {
|
||||
return createToken(authentication, accessTokenValidityInMilliseconds, "access");
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰 생성
|
||||
*/
|
||||
public String createRefreshToken(Authentication authentication) {
|
||||
return createToken(authentication, refreshTokenValidityInMilliseconds, "refresh");
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 정보로 액세스 토큰 생성
|
||||
*/
|
||||
public String createAccessToken(String userId, String username, String roles) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);
|
||||
|
||||
return Jwts.builder()
|
||||
.setSubject(userId)
|
||||
.claim("username", username)
|
||||
.claim("roles", roles)
|
||||
.claim("type", "access")
|
||||
.setIssuedAt(now)
|
||||
.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자 ID 추출
|
||||
*/
|
||||
public String getUserIdFromToken(String token) {
|
||||
Claims claims = parseClaimsFromToken(token);
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자명 추출
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 만료일 추출
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
Claims claims = parseClaimsFromToken(token);
|
||||
return claims.getExpiration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 유효성 검증
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
parseClaimsFromToken(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
log.debug("Invalid JWT token: {}", 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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리프레시 토큰인지 확인
|
||||
*/
|
||||
public boolean isRefreshToken(String token) {
|
||||
try {
|
||||
Claims claims = parseClaimsFromToken(token);
|
||||
return "refresh".equals(claims.get("type", String.class));
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 만료 확인
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
} catch (JwtException | IllegalArgumentException 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.ktds.hi.common.security;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 보안 관련 유틸리티 클래스
|
||||
* 현재 인증된 사용자 정보 조회 등의 기능을 제공
|
||||
*/
|
||||
public final class SecurityUtil {
|
||||
|
||||
private SecurityUtil() {
|
||||
// 인스턴스 생성 방지
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자 ID 조회
|
||||
*/
|
||||
public static Optional<Long> getCurrentUserId() {
|
||||
return getCurrentAuthentication()
|
||||
.map(Authentication::getName)
|
||||
.map(Long::parseLong);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자명 조회
|
||||
*/
|
||||
public static Optional<String> getCurrentUsername() {
|
||||
return getCurrentAuthentication()
|
||||
.map(Authentication::getName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 인증된 사용자의 권한 조회
|
||||
*/
|
||||
public static Set<String> getCurrentUserRoles() {
|
||||
return getCurrentAuthentication()
|
||||
.map(auth -> auth.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet()))
|
||||
.orElse(Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 특정 권한을 가지는지 확인
|
||||
*/
|
||||
public static boolean hasRole(String role) {
|
||||
return getCurrentUserRoles().contains(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 여러 권한 중 하나라도 가지는지 확인
|
||||
*/
|
||||
public static boolean hasAnyRole(String... roles) {
|
||||
Set<String> userRoles = getCurrentUserRoles();
|
||||
for (String role : roles) {
|
||||
if (userRoles.contains(role)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 모든 권한을 가지는지 확인
|
||||
*/
|
||||
public static boolean hasAllRoles(String... roles) {
|
||||
Set<String> userRoles = getCurrentUserRoles();
|
||||
for (String role : roles) {
|
||||
if (!userRoles.contains(role)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 인증되었는지 확인
|
||||
*/
|
||||
public static boolean isAuthenticated() {
|
||||
return getCurrentAuthentication()
|
||||
.map(Authentication::isAuthenticated)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 익명 사용자인지 확인
|
||||
*/
|
||||
public static boolean isAnonymous() {
|
||||
return !isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 Authentication 객체 조회
|
||||
*/
|
||||
public static Optional<Authentication> getCurrentAuthentication() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return Optional.ofNullable(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 리소스 소유자인지 확인
|
||||
*/
|
||||
public static boolean isOwner(Long resourceOwnerId) {
|
||||
return getCurrentUserId()
|
||||
.map(currentUserId -> currentUserId.equals(resourceOwnerId))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 관리자인지 확인
|
||||
*/
|
||||
public static boolean isAdmin() {
|
||||
return hasRole("ROLE_ADMIN");
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 매장 소유자인지 확인
|
||||
*/
|
||||
public static boolean isStoreOwner() {
|
||||
return hasRole("ROLE_STORE_OWNER");
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 사용자가 일반 사용자인지 확인
|
||||
*/
|
||||
public static boolean isUser() {
|
||||
return hasRole("ROLE_USER");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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.repository.AuditLogRepository;
|
||||
import com.ktds.hi.common.security.SecurityUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 감사 로그 서비스
|
||||
* 시스템의 중요한 액션들을 비동기적으로 로깅
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AuditLogService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
|
||||
/**
|
||||
* 감사 로그 기록 (비동기)
|
||||
*/
|
||||
@Async
|
||||
@Transactional
|
||||
public void logAsync(AuditAction action, String entityType, String entityId, String description) {
|
||||
log(action, entityType, entityId, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 감사 로그 기록 (동기)
|
||||
*/
|
||||
@Transactional
|
||||
public void log(AuditAction action, String entityType, String entityId, String description) {
|
||||
try {
|
||||
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
|
||||
String username = SecurityUtil.getCurrentUsername().orElse("SYSTEM");
|
||||
|
||||
AuditLog auditLog = AuditLog.create(userId, username, action, entityType, entityId, description);
|
||||
auditLogRepository.save(auditLog);
|
||||
|
||||
} catch (Exception e) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃 로그
|
||||
*/
|
||||
public void logLogout(String description) {
|
||||
logAsync(AuditAction.LOGOUT, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
spring:
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
jdbc:
|
||||
batch_size: 20
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
batch_versioned_data: true
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# Jackson 설정
|
||||
jackson:
|
||||
time-zone: Asia/Seoul
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
|
||||
# 트랜잭션 설정
|
||||
transaction:
|
||||
default-timeout: 30
|
||||
|
||||
# 애플리케이션 설정
|
||||
app:
|
||||
# JWT 설정
|
||||
jwt:
|
||||
secret-key: ${JWT_SECRET_KEY:hiorder-secret-key-for-jwt-token-generation-2024-very-long-secret-key}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1시간
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
|
||||
|
||||
# CORS 설정
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080}
|
||||
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
|
||||
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
|
||||
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization}
|
||||
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
|
||||
max-age: ${CORS_MAX_AGE:3600}
|
||||
|
||||
# 캐시 설정
|
||||
cache:
|
||||
default-ttl: ${CACHE_DEFAULT_TTL:3600} # 1시간
|
||||
|
||||
# Swagger 설정
|
||||
swagger:
|
||||
title: ${SWAGGER_TITLE:하이오더 API}
|
||||
description: ${SWAGGER_DESCRIPTION:하이오더 백엔드 API 문서}
|
||||
version: ${SWAGGER_VERSION:1.0.0}
|
||||
server-url: ${SWAGGER_SERVER_URL:http://localhost:8080}
|
||||
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
com.ktds.hi: ${LOG_LEVEL:INFO}
|
||||
org.springframework.security: ${SECURITY_LOG_LEVEL:INFO}
|
||||
org.hibernate.SQL: ${SQL_LOG_LEVEL:INFO}
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: ${SQL_PARAM_LOG_LEVEL:INFO}
|
||||
pattern:
|
||||
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
|
||||
# 관리 엔드포인트 설정
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
Reference in New Issue
Block a user