diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java index 613622fa..f85cdcec 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java @@ -1,11 +1,13 @@ package until.the.eternity.auctionhistory.interfaces.rest.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import until.the.eternity.auctionhistory.application.scheduler.AuctionHistoryScheduler; import until.the.eternity.auctionhistory.application.service.AuctionHistoryService; @@ -45,9 +47,11 @@ public ResponseEntity> findById // TODO: 응답 형식도 몇건씩 저장됐는지 결과를 보내줘야하나 고민 // TODO: 실행 시 OPEN API Key를 따로 받을지 고민 @PostMapping("/batch") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @Operation( summary = "경매장 거래 내역 배치 실행", - description = "Nexon Open API 경매장 거래 내역 모든 카테고리 데이터 INSERT 배치 실행") + description = "Nexon Open API 경매장 거래 내역 모든 카테고리 데이터 INSERT 배치 실행. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") public ResponseEntity triggerMinPriceBatch() { scheduler.fetchAndSaveAuctionHistoryAll(); return ResponseEntity.ok().build(); diff --git a/src/main/java/until/the/eternity/common/enums/UserRole.java b/src/main/java/until/the/eternity/common/enums/UserRole.java new file mode 100644 index 00000000..02d2ddd8 --- /dev/null +++ b/src/main/java/until/the/eternity/common/enums/UserRole.java @@ -0,0 +1,28 @@ +package until.the.eternity.common.enums; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum UserRole { + USER("user", "일반 사용자"), + ADMIN("admin", "관리자"), + SUPER_ADMIN("super_admin", "최고 관리자"); + + private final String code; + private final String description; + + private static final Map CODE_MAP = + Arrays.stream(values()) + .collect(Collectors.toMap(UserRole::getCode, Function.identity())); + + public static Optional fromCode(String code) { + return Optional.ofNullable(CODE_MAP.get(code)); + } +} diff --git a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java index ef255e95..678f8ebc 100644 --- a/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java +++ b/src/main/java/until/the/eternity/common/filter/GatewayAuthFilter.java @@ -4,6 +4,9 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -14,17 +17,14 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import until.the.eternity.common.entity.CustomWebAuthenticationDetails; +import until.the.eternity.common.enums.UserRole; import until.the.eternity.common.util.IpAddressUtil; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - /** * Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로 Spring Security의 Authentication을 생성하는 필터 * *

Gateway에서 전달하는 헤더: - X-Auth-User-Id: 사용자 ID (Long) - X-Auth-Username: 사용자 이메일/username - * (String) - X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN) + * (String) - X-Auth-Roles: 사용자 역할 (예: user, admin, super_admin) */ @Slf4j @Component @@ -40,13 +40,12 @@ protected void doFilterInternal( @NonNull FilterChain filterChain) throws ServletException, IOException { - // Gateway에서 전달한 인증 헤더 읽기 String userIdHeader = request.getHeader("X-Auth-User-Id"); String usernameHeader = request.getHeader("X-Auth-Username"); String rolesHeader = request.getHeader("X-Auth-Roles"); UsernamePasswordAuthenticationToken authentication = - getAuthentication(userIdHeader, usernameHeader, rolesHeader); + getAuthentication(userIdHeader, rolesHeader); String clientIp = ipAddressUtil.getClientIp(request); @@ -57,23 +56,18 @@ protected void doFilterInternal( SecurityContextHolder.getContext().setAuthentication(authentication); - log.debug("Authentication set for user: {} with roles: {}", usernameHeader, rolesHeader); + log.debug( + "Authentication set - Principal: {}, Username: {}, Authorities: {}", + authentication.getPrincipal(), + usernameHeader, + authentication.getAuthorities()); filterChain.doFilter(request, response); } - /** - * Gateway에서 전달한 헤더 정보를 기반으로 Authentication 생성 - * - * @param userIdHeader 사용자 ID (Gateway에서 검증됨) - * @param usernameHeader 사용자 이메일/username (Gateway에서 검증됨) - * @param rolesHeader 사용자 역할 (Gateway에서 검증됨) - * @return UsernamePasswordAuthenticationToken - */ private UsernamePasswordAuthenticationToken getAuthentication( - String userIdHeader, String usernameHeader, String rolesHeader) { + String userIdHeader, String rolesHeader) { - // User ID 파싱 Long userId = null; try { if (userIdHeader != null) { @@ -83,28 +77,23 @@ private UsernamePasswordAuthenticationToken getAuthentication( log.warn("Invalid user ID header: {}", userIdHeader); } - // Roles가 없으면 익명 사용자로 처리 if (rolesHeader == null || rolesHeader.isEmpty()) { log.debug("No roles found, creating anonymous authentication"); return new UsernamePasswordAuthenticationToken(userId, null); } - // Roles 파싱 (ROLE_USER, ROLE_ADMIN 등) - List authorities = new ArrayList<>(); + String roleCode = rolesHeader.trim().toLowerCase(); + UserRole userRole = UserRole.fromCode(roleCode).orElse(null); - // Gateway에서 넘어온 role이 이미 "ROLE_" prefix를 가지고 있으므로 그대로 사용 - // 단, "ROLE_" prefix가 없으면 추가 - String role = rolesHeader.trim(); - if (!role.startsWith("ROLE_")) { - role = "ROLE_" + role; + if (userRole == null) { + log.warn("Unknown role code: '{}', creating anonymous authentication", roleCode); + return new UsernamePasswordAuthenticationToken(userId, null); } - authorities.add(new SimpleGrantedAuthority(role)); - log.debug( - "Created authentication for userId: {}, username: {}, role: {}", - userId, - usernameHeader, - role); + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole.name())); + + log.debug("Created authentication for userId: {}, role: {}", userId, userRole.name()); return new UsernamePasswordAuthenticationToken(userId, null, authorities); } diff --git a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java index e8c1fca4..1e26a003 100644 --- a/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java +++ b/src/main/java/until/the/eternity/hornBugle/interfaces/rest/controller/HornBugleController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; @@ -9,6 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import until.the.eternity.common.response.PageResponseDto; @@ -49,7 +51,9 @@ public ResponseEntity> search( } @PostMapping("/batch") - @Operation(summary = "뿔피리 히스토리 배치 실행", description = "모든 서버의 거대한 외침의 뿔피리 내역을 수집하여 저장합니다.") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") + @Operation(summary = "뿔피리 히스토리 배치 실행", description = "모든 서버의 거대한 외침의 뿔피리 내역을 수집하여 저장합니다. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") public ResponseEntity triggerBatch() { log.info("[HornBugle] Batch API triggered"); try { diff --git a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java index a7241238..874e8fbd 100644 --- a/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java +++ b/src/main/java/until/the/eternity/iteminfo/interfaces/rest/controller/ItemInfoController.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import until.the.eternity.common.enums.SortDirection; import until.the.eternity.common.response.ApiResponse; @@ -78,7 +79,10 @@ public ResponseEntity>> getItemInfosSu summary = "경매 내역에서 아이템 정보 동기화", description = "AuctionHistory 테이블에서 아이템 정보를 조회하여 ItemInfo 테이블에 동기화합니다. " - + "이미 존재하는 아이템은 제외하고 새로운 아이템만 추가합니다.") + + "이미 존재하는 아이템은 제외하고 새로운 아이템만 추가합니다. " + + "**[ADMIN, SUPER_ADMIN 전용]**") + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @PostMapping("/sync") public ResponseEntity> syncItemInfoFromAuctionHistory() { ItemInfoSyncResponse response = itemInfoService.syncItemInfoFromAuctionHistory(); diff --git a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java index ff8327d9..f7e1124b 100644 --- a/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java +++ b/src/main/java/until/the/eternity/itemoptioninfo/interfaces/rest/controller/ItemOptionInfoController.java @@ -1,10 +1,12 @@ package until.the.eternity.itemoptioninfo.interfaces.rest.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import until.the.eternity.itemoptioninfo.application.service.ItemOptionInfoService; import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo; @@ -35,7 +37,9 @@ public List findAll() { @PostMapping @ResponseStatus(HttpStatus.CREATED) - @Operation(summary = "아이템 옵션 정보 생성", description = "새로운 아이템 옵션 정보를 생성합니다.") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") + @Operation(summary = "아이템 옵션 정보 생성", description = "새로운 아이템 옵션 정보를 생성합니다. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") public ItemOptionInfoResponse create(@Valid @RequestBody ItemOptionInfoRequest request) { ItemOptionInfo itemOptionInfo = itemOptionInfoMapper.toEntity(request); ItemOptionInfo saved = itemOptionInfoService.create(itemOptionInfo); @@ -43,10 +47,12 @@ public ItemOptionInfoResponse create(@Valid @RequestBody ItemOptionInfoRequest r } @PutMapping + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @Operation( summary = "아이템 옵션 정보 수정", description = - "아이템 옵션 정보를 수정합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별하며, optionDesc만 수정 가능합니다.") + "아이템 옵션 정보를 수정합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별하며, optionDesc만 수정 가능합니다. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") public ItemOptionInfoResponse update(@Valid @RequestBody ItemOptionInfoRequest request) { ItemOptionInfoId id = itemOptionInfoMapper.toId(request); ItemOptionInfo updated = itemOptionInfoService.update(id, request.getOptionDesc()); @@ -55,10 +61,12 @@ public ItemOptionInfoResponse update(@Valid @RequestBody ItemOptionInfoRequest r @DeleteMapping @ResponseStatus(HttpStatus.NO_CONTENT) + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @Operation( summary = "아이템 옵션 정보 삭제", description = - "아이템 옵션 정보를 삭제합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별합니다.") + "아이템 옵션 정보를 삭제합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별합니다. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") public void delete(@Valid @RequestBody ItemOptionInfoRequest request) { ItemOptionInfoId id = itemOptionInfoMapper.toId(request); itemOptionInfoService.delete(id); diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareAttributeInfoController.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareAttributeInfoController.java index 2320eed1..ecc57760 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareAttributeInfoController.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareAttributeInfoController.java @@ -1,11 +1,13 @@ package until.the.eternity.metalwareinfo.interfaces.rest.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import until.the.eternity.common.response.PageResponseDto; import until.the.eternity.metalwareinfo.application.service.MetalwareAttributeInfoService; @@ -20,7 +22,9 @@ public class MetalwareAttributeInfoController { private final MetalwareAttributeInfoService metalwareAttributeInfoService; - @Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다.") + @Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다. **[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @PostMapping("/sync") public ResponseEntity sync() { int syncedCount = metalwareAttributeInfoService.sync(); diff --git a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java index 47e6c9a3..5917ed61 100644 --- a/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java +++ b/src/main/java/until/the/eternity/metalwareinfo/interfaces/rest/controller/MetalwareInfoController.java @@ -1,9 +1,11 @@ package until.the.eternity.metalwareinfo.interfaces.rest.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,7 +35,10 @@ public List getAllMetalwareInfos() { summary = "세공 정보 동기화", description = "metalware_attribute_info를 기반으로 metalware_info를 업서트합니다. " - + "레벨 1 attribute 동기화와 금속별 최대 레벨(limit_break_level) 동기화를 한 번에 수행합니다.") + + "레벨 1 attribute 동기화와 금속별 최대 레벨(limit_break_level) 동기화를 한 번에 수행합니다. " + + "**[ADMIN, SUPER_ADMIN 전용]**") + @ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") @PostMapping("/sync") public ResponseEntity syncMetalwareInfo() { return ResponseEntity.ok(metalwareInfoService.syncFromAttributeInfo());