From 432155a8bc0523f845e0fd0da2cb074a21c8730d Mon Sep 17 00:00:00 2001 From: Hyojin Ahn Date: Fri, 21 Nov 2025 10:20:25 -0500 Subject: [PATCH] =?UTF-8?q?=ED=86=A0=ED=81=B0=20exception=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20page=20=EC=B6=94=EA=B0=80=20dto=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 34 ++++++- .../common/exception/JwtExpiredException.java | 27 ++++++ .../common/exception/JwtInvalidException.java | 27 ++++++ .../erp/config/JwtAuthenticationFilter.java | 67 +++++++++---- .../goi/erp/controller/AdminController.java | 40 -------- .../erp/controller/CustomerController.java | 96 ++++++++++++++----- .../erp/repository/CustomerRepository.java | 23 ++++- .../com/goi/erp/service/CustomerService.java | 18 ++-- .../token/PermissionAuthenticationToken.java | 23 +++++ src/main/resources/application.yml | 4 + 10 files changed, 265 insertions(+), 94 deletions(-) create mode 100644 src/main/java/com/goi/erp/common/exception/JwtExpiredException.java create mode 100644 src/main/java/com/goi/erp/common/exception/JwtInvalidException.java delete mode 100644 src/main/java/com/goi/erp/controller/AdminController.java create mode 100644 src/main/java/com/goi/erp/token/PermissionAuthenticationToken.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 index 474a63b..70912b0 100644 --- a/src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/goi/erp/common/exception/GlobalExceptionHandler.java @@ -24,9 +24,39 @@ public class GlobalExceptionHandler { return ResponseEntity.badRequest().body(body); } + // 모든 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity> handleAllExceptions(Exception ex) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); + body.put("error", "Internal Server Error"); + body.put("message", ex.getMessage()); + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 권한 없음 @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDenied(AccessDeniedException ex) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); + public ResponseEntity> handleAccessDenied(AccessDeniedException ex) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.FORBIDDEN.value()); + body.put("error", "Forbidden"); + body.put("message", ex.getMessage()); + return new ResponseEntity<>(body, HttpStatus.FORBIDDEN); + } + + // + @ExceptionHandler(JwtExpiredException.class) + public ResponseEntity> handleJwtExpired(JwtExpiredException ex) { + Map body = Map.of("error", "JWT expired", "message", ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body); + } + + @ExceptionHandler(JwtInvalidException.class) + public ResponseEntity> handleJwtInvalid(JwtInvalidException ex) { + Map body = Map.of("error", "Invalid JWT", "message", ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body); } } diff --git a/src/main/java/com/goi/erp/common/exception/JwtExpiredException.java b/src/main/java/com/goi/erp/common/exception/JwtExpiredException.java new file mode 100644 index 0000000..134ab08 --- /dev/null +++ b/src/main/java/com/goi/erp/common/exception/JwtExpiredException.java @@ -0,0 +1,27 @@ +package com.goi.erp.common.exception; + +public class JwtExpiredException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public JwtExpiredException() { + super(); + } + + public JwtExpiredException(String message) { + super(message); + } + + public JwtExpiredException(String message, Throwable cause) { + super(message, cause); + } + + public JwtExpiredException(Throwable cause) { + super(cause); + } + + protected JwtExpiredException(String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/goi/erp/common/exception/JwtInvalidException.java b/src/main/java/com/goi/erp/common/exception/JwtInvalidException.java new file mode 100644 index 0000000..06cb162 --- /dev/null +++ b/src/main/java/com/goi/erp/common/exception/JwtInvalidException.java @@ -0,0 +1,27 @@ +package com.goi.erp.common.exception; + +public class JwtInvalidException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public JwtInvalidException() { + super(); + } + + public JwtInvalidException(String message) { + super(message); + } + + public JwtInvalidException(String message, Throwable cause) { + super(message, cause); + } + + public JwtInvalidException(Throwable cause) { + super(cause); + } + + protected JwtInvalidException(String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/goi/erp/config/JwtAuthenticationFilter.java b/src/main/java/com/goi/erp/config/JwtAuthenticationFilter.java index 16e2f06..c1b7fd7 100644 --- a/src/main/java/com/goi/erp/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/goi/erp/config/JwtAuthenticationFilter.java @@ -1,12 +1,13 @@ package com.goi.erp.config; +import com.goi.erp.common.permission.PermissionSet; import com.goi.erp.token.JwtService; +import com.goi.erp.token.PermissionAuthenticationToken; +import io.jsonwebtoken.ExpiredJwtException; import lombok.RequiredArgsConstructor; import org.springframework.lang.NonNull; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -39,24 +40,52 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { final String jwt = authHeader.substring(7); - if (SecurityContextHolder.getContext().getAuthentication() == null && jwtService.isTokenValid(jwt)) { + try { + // 기존 인증 정보 확인 + var authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean needsAuthentication = true; - // 토큰에서 empUuid와 권한 추출 - String empUuid = jwtService.extractEmpUuid(jwt); - List permissions = jwtService.extractPermissions(jwt); - List authorities = permissions.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); + if (authentication instanceof PermissionAuthenticationToken token) { + // PermissionSet이 이미 존재하면 새로 세팅할 필요 없음 + needsAuthentication = token.getPermissionSet() == null; + } else if (authentication != null) { + // 다른 타입의 Authentication이 존재하면 덮어쓰지 않음 + needsAuthentication = false; + } - // 인증 정보 SecurityContextHolder에 세팅 - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( - empUuid, // principal - null, // credentials - authorities - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); + if (needsAuthentication && jwtService.isTokenValid(jwt)) { + + // 토큰에서 empUuid와 PermissionSet 추출 + String empUuid = jwtService.extractEmpUuid(jwt); + PermissionSet permissionSet = jwtService.getPermissions(jwt); + + if (permissionSet == null) { + permissionSet = new PermissionSet(List.of()); // 빈 PermissionSet으로 초기화 + } + + // SimpleGrantedAuthority 생성 + List authorities = permissionSet.permissions().stream() + .map(p -> new SimpleGrantedAuthority(p.toString())) // 필요시 커스텀 문자열로 변경 + .collect(Collectors.toList()); + + // PermissionAuthenticationToken 생성 + PermissionAuthenticationToken authToken = + new PermissionAuthenticationToken(empUuid, permissionSet, authorities); + + // SecurityContextHolder에 세팅 + SecurityContextHolder.getContext().setAuthentication(authToken); + } + + } catch (ExpiredJwtException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"error\":\"Session has expired.\"}"); + return; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"error\":\"Invalid login information.\"}"); + return; } filterChain.doFilter(request, response); diff --git a/src/main/java/com/goi/erp/controller/AdminController.java b/src/main/java/com/goi/erp/controller/AdminController.java deleted file mode 100644 index 1e7d5a9..0000000 --- a/src/main/java/com/goi/erp/controller/AdminController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.goi.erp.controller; - -import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/admin") -@PreAuthorize("hasRole('ADMIN')") -public class AdminController { - - @GetMapping - @PreAuthorize("hasAuthority('admin:read')") - public String get() { - return "GET:: admin controller"; - } - @PostMapping - @PreAuthorize("hasAuthority('admin:create')") - @Hidden - public String post() { - return "POST:: admin controller"; - } - @PutMapping - @PreAuthorize("hasAuthority('admin:update')") - @Hidden - public String put() { - return "PUT:: admin controller"; - } - @DeleteMapping - @PreAuthorize("hasAuthority('admin:delete')") - @Hidden - public String delete() { - return "DELETE:: admin controller"; - } -} diff --git a/src/main/java/com/goi/erp/controller/CustomerController.java b/src/main/java/com/goi/erp/controller/CustomerController.java index 5ea1c82..205506b 100644 --- a/src/main/java/com/goi/erp/controller/CustomerController.java +++ b/src/main/java/com/goi/erp/controller/CustomerController.java @@ -5,21 +5,32 @@ 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; +import com.goi.erp.token.PermissionAuthenticationToken; import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; 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; import java.util.UUID; @RestController @RequestMapping("/customer") @RequiredArgsConstructor public class CustomerController { + @Value("${pagination.default-page:0}") + private int defaultPage; + + @Value("${pagination.default-size:20}") + private int defaultSize; + + @Value("${pagination.max-size:100}") + private int maxSize; private final CustomerService customerService; @@ -27,10 +38,16 @@ public class CustomerController { @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"); - } + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canCreateCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } CustomerResponseDto responseDto = customerService.createCustomer(requestDto); return new ResponseEntity<>(responseDto, HttpStatus.CREATED); @@ -38,24 +55,45 @@ public class CustomerController { // READ ALL @GetMapping - public ResponseEntity> getAllCustomers() { + public ResponseEntity> getAllCustomers( + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer size + ) { // 권한 체크 - PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); - if (!PermissionChecker.canReadCRMAll(permissionSet)) { - throw new AccessDeniedException("You do not have permission to read all CRM data"); - } + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadCRMAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } - return ResponseEntity.ok(customerService.getAllCustomers()); + // + int p = (page == null) ? defaultPage : page; + int s = (size == null) ? defaultSize : size; + if (s > maxSize) s = maxSize; + + // + return ResponseEntity.ok(customerService.getAllCustomers(p, s)); } // 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"); - } + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } return ResponseEntity.ok(customerService.getCustomerByUuid(uuid)); } @@ -66,10 +104,16 @@ public class CustomerController { @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"); - } + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canUpdateCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } return ResponseEntity.ok(customerService.updateCustomer(uuid, requestDto)); } @@ -78,10 +122,16 @@ public class CustomerController { @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"); - } + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canDeleteCRM(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } customerService.deleteCustomer(uuid); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/goi/erp/repository/CustomerRepository.java b/src/main/java/com/goi/erp/repository/CustomerRepository.java index ec7b37f..8d07dc4 100644 --- a/src/main/java/com/goi/erp/repository/CustomerRepository.java +++ b/src/main/java/com/goi/erp/repository/CustomerRepository.java @@ -1,7 +1,12 @@ package com.goi.erp.repository; +import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.entity.Customer; + +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.stereotype.Repository; import java.util.Optional; @@ -9,5 +14,21 @@ import java.util.UUID; @Repository public interface CustomerRepository extends JpaRepository { - Optional findByCusUuid(UUID uuid); + @Query("SELECT new com.goi.erp.dto.CustomerResponseDto(" + + "c.cusUuid, c.cusNo, c.cusName, c.cusStatus, c.cusAreaId, " + + "c.cusAddress1, c.cusAddress2, c.cusPostalCode, c.cusCity, c.cusProvince, " + + "c.cusGeoLat, c.cusGeoLon, c.cusEmail, c.cusPhone, c.cusPhoneExt, " + + "c.cusOpenTime, c.cusNote, c.cusContractDate, c.cusContractedBy, c.cusInstallDate, c.cusInstallLocatoin) " + + "FROM Customer c WHERE c.cusUuid = :uuid") + Optional findCustomerDtoByCusUuid(UUID uuid); + + @Query("SELECT new com.goi.erp.dto.CustomerResponseDto(" + + "c.cusUuid, c.cusNo, c.cusName, c.cusStatus, c.cusAreaId, " + + "c.cusAddress1, c.cusAddress2, c.cusPostalCode, c.cusCity, c.cusProvince, " + + "c.cusGeoLat, c.cusGeoLon, c.cusEmail, c.cusPhone, c.cusPhoneExt, " + + "c.cusOpenTime, c.cusNote, c.cusContractDate, c.cusContractedBy, c.cusInstallDate, c.cusInstallLocatoin) " + + "FROM Customer c") + Page findAllCustomerDtos(Pageable pageable); + + Optional findByCusUuid(UUID cusUuid); } diff --git a/src/main/java/com/goi/erp/service/CustomerService.java b/src/main/java/com/goi/erp/service/CustomerService.java index f76b4b8..15b64df 100644 --- a/src/main/java/com/goi/erp/service/CustomerService.java +++ b/src/main/java/com/goi/erp/service/CustomerService.java @@ -5,11 +5,13 @@ import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.entity.Customer; import com.goi.erp.repository.CustomerRepository; import lombok.RequiredArgsConstructor; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -29,19 +31,17 @@ public class CustomerService { .cusProvince(dto.getCusProvince()) .build(); customer = customerRepository.save(customer); - return mapToDto(customer); + return mapToDto(customer); // 생성 시에는 여전히 엔티티 → DTO 필요 } - public List getAllCustomers() { - return customerRepository.findAll().stream() - .map(this::mapToDto) - .collect(Collectors.toList()); + public Page getAllCustomers(int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return customerRepository.findAllCustomerDtos(pageable); // DTO 바로 조회 } public CustomerResponseDto getCustomerByUuid(UUID uuid) { - Customer customer = customerRepository.findByCusUuid(uuid) + return customerRepository.findCustomerDtoByCusUuid(uuid) .orElseThrow(() -> new RuntimeException("Customer not found")); - return mapToDto(customer); } public CustomerResponseDto updateCustomer(UUID uuid, CustomerRequestDto dto) { diff --git a/src/main/java/com/goi/erp/token/PermissionAuthenticationToken.java b/src/main/java/com/goi/erp/token/PermissionAuthenticationToken.java new file mode 100644 index 0000000..3a3bdf0 --- /dev/null +++ b/src/main/java/com/goi/erp/token/PermissionAuthenticationToken.java @@ -0,0 +1,23 @@ +package com.goi.erp.token; + +import java.util.Collection; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import com.goi.erp.common.permission.PermissionSet; + +public class PermissionAuthenticationToken extends UsernamePasswordAuthenticationToken { + private static final long serialVersionUID = 1L; + private final PermissionSet permissionSet; + + public PermissionAuthenticationToken(String principal, PermissionSet permissionSet, Collection authorities) { + super(principal, null, authorities); + this.permissionSet = permissionSet; + } + + public PermissionSet getPermissionSet() { + return permissionSet; + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 65aeaac..3fcc8e9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,6 +22,10 @@ application: expiration: 86400000 # a day refresh-token: expiration: 604800000 # 7 days +pagination: + default-page: 0 + default-size: 20 + max-size: 100 server: port: 8082 servlet: