From e51837e6dde3127901ec1b0597585391f236f643 Mon Sep 17 00:00:00 2001 From: Hyojin Ahn Date: Thu, 20 Nov 2025 08:59:50 -0500 Subject: [PATCH] =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 32 +++++++++++++++++ .../goi/erp/common/permission/Permission.java | 18 ++++++++++ .../common/permission/PermissionChecker.java | 36 +++++++++++++++++++ .../common/permission/PermissionEnums.java | 22 ++++++++++++ .../common/permission/PermissionParser.java | 32 +++++++++++++++++ .../erp/common/permission/PermissionSet.java | 26 ++++++++++++++ .../erp/controller/CustomerController.java | 34 ++++++++++++++++++ .../java/com/goi/erp/token/JwtService.java | 17 +++++++-- 8 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/goi/erp/common/permission/Permission.java create mode 100644 src/main/java/com/goi/erp/common/permission/PermissionChecker.java create mode 100644 src/main/java/com/goi/erp/common/permission/PermissionEnums.java create mode 100644 src/main/java/com/goi/erp/common/permission/PermissionParser.java create mode 100644 src/main/java/com/goi/erp/common/permission/PermissionSet.java diff --git a/src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java b/src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..474a63b --- /dev/null +++ b/src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +package com.goi.erp.common.exception; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException ex) { + + Map body = new HashMap<>(); + body.put("error", ex.getMessage()); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + + return ResponseEntity.badRequest().body(body); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDenied(AccessDeniedException ex) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); + } +} + diff --git a/src/main/java/com/goi/erp/common/permission/Permission.java b/src/main/java/com/goi/erp/common/permission/Permission.java new file mode 100644 index 0000000..cea0537 --- /dev/null +++ b/src/main/java/com/goi/erp/common/permission/Permission.java @@ -0,0 +1,18 @@ +package com.goi.erp.common.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Permission { + private PermissionEnums.Module module; + private PermissionEnums.Action action; + private PermissionEnums.Scope scope; + private final boolean all; + + public boolean isAll() { + return all || module == PermissionEnums.Module.ALL; + } +} + diff --git a/src/main/java/com/goi/erp/common/permission/PermissionChecker.java b/src/main/java/com/goi/erp/common/permission/PermissionChecker.java new file mode 100644 index 0000000..31a3387 --- /dev/null +++ b/src/main/java/com/goi/erp/common/permission/PermissionChecker.java @@ -0,0 +1,36 @@ +package com.goi.erp.common.permission; + +public class PermissionChecker { + + public static boolean canCreateCRM(PermissionSet set) { + if (set.hasAll()) return true; + return set.has(PermissionEnums.Module.C, PermissionEnums.Action.C); + } + + public static boolean canReadCRM(PermissionSet set) { + if (set.hasAll()) return true; + return set.has(PermissionEnums.Module.C, PermissionEnums.Action.R); + } + + public static boolean canUpdateCRM(PermissionSet set) { + if (set.hasAll()) return true; + return set.has(PermissionEnums.Module.C, PermissionEnums.Action.U); + } + + public static boolean canDeleteCRM(PermissionSet set) { + if (set.hasAll()) return true; + return set.has(PermissionEnums.Module.C, PermissionEnums.Action.D); + } + + // 범위까지 체크 + public static boolean canReadCRMAll(PermissionSet set) { + if (set.hasAll()) return true; + return set.hasFull( + PermissionEnums.Module.C, + PermissionEnums.Action.R, + PermissionEnums.Scope.A + ); + } +} + + diff --git a/src/main/java/com/goi/erp/common/permission/PermissionEnums.java b/src/main/java/com/goi/erp/common/permission/PermissionEnums.java new file mode 100644 index 0000000..00562cc --- /dev/null +++ b/src/main/java/com/goi/erp/common/permission/PermissionEnums.java @@ -0,0 +1,22 @@ +package com.goi.erp.common.permission; + +public class PermissionEnums { + + public enum Module { + H, // HCM + C, // CRM + A, // ACC + O, // OPERATION + S, // SYSTEM + ALL // ADMIN + } + + public enum Action { + C, R, U, D + } + + public enum Scope { + S, P, A + } +} + diff --git a/src/main/java/com/goi/erp/common/permission/PermissionParser.java b/src/main/java/com/goi/erp/common/permission/PermissionParser.java new file mode 100644 index 0000000..14a5bf3 --- /dev/null +++ b/src/main/java/com/goi/erp/common/permission/PermissionParser.java @@ -0,0 +1,32 @@ +package com.goi.erp.common.permission; + +import java.util.ArrayList; +import java.util.List; + +public class PermissionParser { + + public static PermissionSet parse(List permissionStrings) { + + List list = new ArrayList<>(); + + for (String str : permissionStrings) { + // ALL 권한 추가 + if ("ALL".equalsIgnoreCase(str)) { + list.add(new Permission(PermissionEnums.Module.ALL, null, null, true)); + continue; + } + // 문자 세개 조합 인지 확인 + String[] parts = str.split(":"); + if (parts.length != 3) continue; + + PermissionEnums.Module module = PermissionEnums.Module.valueOf(parts[0]); + PermissionEnums.Action action = PermissionEnums.Action.valueOf(parts[1]); + PermissionEnums.Scope scope = PermissionEnums.Scope.valueOf(parts[2]); + // + list.add(new Permission(module, action, scope, false)); + } + + return new PermissionSet(list); + } +} + diff --git a/src/main/java/com/goi/erp/common/permission/PermissionSet.java b/src/main/java/com/goi/erp/common/permission/PermissionSet.java new file mode 100644 index 0000000..a5dc68a --- /dev/null +++ b/src/main/java/com/goi/erp/common/permission/PermissionSet.java @@ -0,0 +1,26 @@ +package com.goi.erp.common.permission; + +import java.util.List; + +public record PermissionSet(List permissions) { + + public boolean has(PermissionEnums.Module module, + PermissionEnums.Action action) { + return permissions.stream() + .anyMatch(p -> p.getModule() == module && + p.getAction() == action); + } + + public boolean hasFull(PermissionEnums.Module module, + PermissionEnums.Action action, + PermissionEnums.Scope scope) { + return permissions.stream() + .anyMatch(p -> p.getModule() == module && + p.getAction() == action && + p.getScope().ordinal() >= scope.ordinal()); + } + + public boolean hasAll() { + return permissions.stream().anyMatch(p -> p.isAll()); + } +} diff --git a/src/main/java/com/goi/erp/controller/CustomerController.java b/src/main/java/com/goi/erp/controller/CustomerController.java index b450af5..5ea1c82 100644 --- a/src/main/java/com/goi/erp/controller/CustomerController.java +++ b/src/main/java/com/goi/erp/controller/CustomerController.java @@ -1,5 +1,7 @@ package com.goi.erp.controller; +import com.goi.erp.common.permission.PermissionChecker; +import com.goi.erp.common.permission.PermissionSet; import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.service.CustomerService; @@ -7,6 +9,8 @@ import com.goi.erp.service.CustomerService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -22,6 +26,12 @@ public class CustomerController { // CREATE @PostMapping public ResponseEntity createCustomer(@RequestBody CustomerRequestDto requestDto) { + // 권한 체크 + PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); + if (!PermissionChecker.canCreateCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to create CRM data"); + } + CustomerResponseDto responseDto = customerService.createCustomer(requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); } @@ -29,12 +39,24 @@ public class CustomerController { // READ ALL @GetMapping public ResponseEntity> getAllCustomers() { + // 권한 체크 + PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); + if (!PermissionChecker.canReadCRMAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } + return ResponseEntity.ok(customerService.getAllCustomers()); } // READ ONE @GetMapping("/{uuid}") public ResponseEntity getCustomer(@PathVariable UUID uuid) { + // 권한 체크 + PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); + if (!PermissionChecker.canReadCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read CRM data"); + } + return ResponseEntity.ok(customerService.getCustomerByUuid(uuid)); } @@ -43,12 +65,24 @@ public class CustomerController { public ResponseEntity updateCustomer( @PathVariable UUID uuid, @RequestBody CustomerRequestDto requestDto) { + // 권한 체크 + PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); + if (!PermissionChecker.canUpdateCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to update CRM data"); + } + return ResponseEntity.ok(customerService.updateCustomer(uuid, requestDto)); } // DELETE @DeleteMapping("/{uuid}") public ResponseEntity deleteCustomer(@PathVariable UUID uuid) { + // 권한 체크 + PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); + if (!PermissionChecker.canDeleteCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to delete CRM data"); + } + customerService.deleteCustomer(uuid); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/com/goi/erp/token/JwtService.java b/src/main/java/com/goi/erp/token/JwtService.java index 8f81697..94894d6 100644 --- a/src/main/java/com/goi/erp/token/JwtService.java +++ b/src/main/java/com/goi/erp/token/JwtService.java @@ -7,6 +7,9 @@ import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import com.goi.erp.common.permission.PermissionParser; +import com.goi.erp.common.permission.PermissionSet; + import java.security.Key; import java.util.List; import java.util.function.Function; @@ -95,6 +98,16 @@ public class JwtService { .getBody(); } + /** + * Permission Set 변환 + */ + @SuppressWarnings("unchecked") + public PermissionSet getPermissions(String token) { + Claims claims = extractAllClaims(token); + List permissions = claims.get("permissions", List.class); + return PermissionParser.parse(permissions); + } + private Key getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); // auth-service와 동일한 Base64 secret return Keys.hmacShaKeyFor(keyBytes); @@ -104,7 +117,7 @@ public class JwtService { JwtService jwtService = new JwtService(); jwtService.secretKey = "D0HaHnTPKLkUO9ULL1Ulm6XDZjhzuFtvTCcxTxSoCS8="; - String token = "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdE5hbWUiOiJIeW9KaW4iLCJsYXN0TmFtZSI6IkFobiIsInBlcm1pc3Npb25zIjpbIkFMTCJdLCJyb2xlcyI6WyJBZG1pbiJdLCJzdWIiOiJhMjM4ZWQ0OC03OGRjLTQ2YWEtOWNiMC1hNWYxZGQyZDJmMTMiLCJpYXQiOjE3NjM1NzE2ODYsImV4cCI6MTc2MzY1ODA4Nn0.0fQT-I4dqHMapIEbxfy8X_SdCReYhUy6djnXUzw4gWc"; + String token = "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdE5hbWUiOiJIeW9qaW4iLCJsYXN0TmFtZSI6IkFobiIsInBlcm1pc3Npb25zIjpbIkFMTCJdLCJyb2xlcyI6WyJBZG1pbiJdLCJzdWIiOiJhMjM4ZWQ0OC03OGRjLTQ2YWEtOWNiMC1hNWYxZGQyZDJmMTMiLCJpYXQiOjE3NjM2NDYzNzQsImV4cCI6MTc2MzczMjc3NH0.HwFZeNbxVldcBeiqc3TUO3x5on2Quy7_Yd_HTkIfOR4"; // user 정보 Claims claims = jwtService.extractAllClaims(token); @@ -115,7 +128,7 @@ public class JwtService { System.out.println("IssuedAt: " + claims.getIssuedAt()); System.out.println("Expiration: " + claims.getExpiration()); System.out.println("FirstName: " + claims.get("firstName", String.class)); - System.out.println("LastName: " + claims.get("LastName", String.class)); + System.out.println("LastName: " + claims.get("lastName", String.class)); // 모든 Claims 확인 // Claims claims = Jwts.parserBuilder()