-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 동기화 및 배치 실행 API PreAuthorize 구현 #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String, UserRole> CODE_MAP = | ||
| Arrays.stream(values()) | ||
| .collect(Collectors.toMap(UserRole::getCode, Function.identity())); | ||
|
|
||
| public static Optional<UserRole> fromCode(String code) { | ||
| return Optional.ofNullable(CODE_MAP.get(code)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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을 생성하는 필터 | ||
| * | ||
| * <p>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<GrantedAuthority> 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<GrantedAuthority> 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); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,13 +2,15 @@ | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||
| 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<PageResponseDto<HornBugleHistoryResponse>> search( | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @PostMapping("/batch") | ||||||||||||||||||||||||||
| @Operation(summary = "뿔피리 히스토리 배치 실행", description = "모든 서버의 거대한 외침의 뿔피리 내역을 수집하여 저장합니다.") | ||||||||||||||||||||||||||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | |
| @PreAuthorize( | |
| "authentication != null && " + | |
| "authentication.authorities.?[" + | |
| " T(org.springframework.util.StringUtils)" + | |
| " .commaDelimitedListToSet(authority)" + | |
| " .contains('ROLE_ADMIN') " + | |
| " or " + | |
| " T(org.springframework.util.StringUtils)" + | |
| " .commaDelimitedListToSet(authority)" + | |
| " .contains('ROLE_SUPER_ADMIN')" + | |
| "].size() > 0") |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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<ApiResponse<List<ItemInfoSummaryResponse>>> 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')") | ||||||
|
||||||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | |
| @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,18 +37,22 @@ public List<ItemOptionInfoResponse> 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 전용)") | ||
|
Comment on lines
39
to
+42
|
||
| public ItemOptionInfoResponse create(@Valid @RequestBody ItemOptionInfoRequest request) { | ||
| ItemOptionInfo itemOptionInfo = itemOptionInfoMapper.toEntity(request); | ||
| ItemOptionInfo saved = itemOptionInfoService.create(itemOptionInfo); | ||
| return itemOptionInfoMapper.toItemOptionInfoResponse(saved); | ||
| } | ||
|
|
||
| @PutMapping | ||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | ||
| @Operation( | ||
|
Comment on lines
49
to
51
|
||
| 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 = "아이템 옵션 정보 삭제", | ||
|
Comment on lines
63
to
66
|
||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -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')") | ||||||||
|
||||||||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | |
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN') or " + | |
| "(authentication != null and authentication.authorities.?[authority != null and (authority.contains('ROLE_ADMIN') or authority.contains('ROLE_SUPER_ADMIN'))].size() > 0)") |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<MetalwareInfoResponse> 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')") | ||||||
|
||||||
| @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") | |
| @PreAuthorize("hasAuthority('ROLE_ADMIN, ROLE_SUPER_ADMIN')") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasAnyRole('ADMIN','SUPER_ADMIN')relies on roles being mapped as separate authorities (ROLE_ADMIN/ROLE_SUPER_ADMIN).GatewayAuthFiltercurrently readsX-Auth-Rolesas a single role string and doesn’t split multi-role headers (even though its header example suggests multiple roles), which can cause admins to be denied here. Please update role parsing (comma-split) or enforce a single-role header contract.