토큰 exception 추가

page 추가
dto 설정 추가
This commit is contained in:
Hyojin Ahn 2025-11-21 10:20:25 -05:00
parent e51837e6dd
commit 432155a8bc
10 changed files with 265 additions and 94 deletions

View File

@ -24,9 +24,39 @@ public class GlobalExceptionHandler {
return ResponseEntity.badRequest().body(body); return ResponseEntity.badRequest().body(body);
} }
// 모든 예외 처리
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAllExceptions(Exception ex) {
Map<String, Object> 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) @ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleAccessDenied(AccessDeniedException ex) { public ResponseEntity<Map<String, Object>> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); Map<String, Object> 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<Map<String, String>> handleJwtExpired(JwtExpiredException ex) {
Map<String, String> body = Map.of("error", "JWT expired", "message", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
@ExceptionHandler(JwtInvalidException.class)
public ResponseEntity<Map<String, String>> handleJwtInvalid(JwtInvalidException ex) {
Map<String, String> body = Map.of("error", "Invalid JWT", "message", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
} }
} }

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,12 +1,13 @@
package com.goi.erp.config; package com.goi.erp.config;
import com.goi.erp.common.permission.PermissionSet;
import com.goi.erp.token.JwtService; import com.goi.erp.token.JwtService;
import com.goi.erp.token.PermissionAuthenticationToken;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull; 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.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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -39,24 +40,52 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
final String jwt = authHeader.substring(7); final String jwt = authHeader.substring(7);
if (SecurityContextHolder.getContext().getAuthentication() == null && jwtService.isTokenValid(jwt)) { try {
// 기존 인증 정보 확인
var authentication = SecurityContextHolder.getContext().getAuthentication();
boolean needsAuthentication = true;
// 토큰에서 empUuid와 권한 추출 if (authentication instanceof PermissionAuthenticationToken token) {
String empUuid = jwtService.extractEmpUuid(jwt); // PermissionSet이 이미 존재하면 새로 세팅할 필요 없음
List<String> permissions = jwtService.extractPermissions(jwt); needsAuthentication = token.getPermissionSet() == null;
List<SimpleGrantedAuthority> authorities = permissions.stream() } else if (authentication != null) {
.map(SimpleGrantedAuthority::new) // 다른 타입의 Authentication이 존재하면 덮어쓰지 않음
.collect(Collectors.toList()); needsAuthentication = false;
}
// 인증 정보 SecurityContextHolder에 세팅 if (needsAuthentication && jwtService.isTokenValid(jwt)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken( // 토큰에서 empUuid와 PermissionSet 추출
empUuid, // principal String empUuid = jwtService.extractEmpUuid(jwt);
null, // credentials PermissionSet permissionSet = jwtService.getPermissions(jwt);
authorities
); if (permissionSet == null) {
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); permissionSet = new PermissionSet(List.of()); // PermissionSet으로 초기화
SecurityContextHolder.getContext().setAuthentication(authToken); }
// SimpleGrantedAuthority 생성
List<SimpleGrantedAuthority> 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); filterChain.doFilter(request, response);

View File

@ -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";
}
}

View File

@ -5,21 +5,32 @@ import com.goi.erp.common.permission.PermissionSet;
import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerRequestDto;
import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.service.CustomerService; import com.goi.erp.service.CustomerService;
import com.goi.erp.token.PermissionAuthenticationToken;
import lombok.RequiredArgsConstructor; 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/customer") @RequestMapping("/customer")
@RequiredArgsConstructor @RequiredArgsConstructor
public class CustomerController { 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; private final CustomerService customerService;
@ -27,10 +38,16 @@ public class CustomerController {
@PostMapping @PostMapping
public ResponseEntity<CustomerResponseDto> createCustomer(@RequestBody CustomerRequestDto requestDto) { public ResponseEntity<CustomerResponseDto> createCustomer(@RequestBody CustomerRequestDto requestDto) {
// 권한 체크 // 권한 체크
PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (!PermissionChecker.canCreateCRM(permissionSet)) {
throw new AccessDeniedException("You do not have permission to create CRM data"); 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); CustomerResponseDto responseDto = customerService.createCustomer(requestDto);
return new ResponseEntity<>(responseDto, HttpStatus.CREATED); return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
@ -38,24 +55,45 @@ public class CustomerController {
// READ ALL // READ ALL
@GetMapping @GetMapping
public ResponseEntity<List<CustomerResponseDto>> getAllCustomers() { public ResponseEntity<Page<CustomerResponseDto>> getAllCustomers(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size
) {
// 권한 체크 // 권한 체크
PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (!PermissionChecker.canReadCRMAll(permissionSet)) {
throw new AccessDeniedException("You do not have permission to read all CRM data");
}
return ResponseEntity.ok(customerService.getAllCustomers()); 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");
}
//
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 // READ ONE
@GetMapping("/{uuid}") @GetMapping("/{uuid}")
public ResponseEntity<CustomerResponseDto> getCustomer(@PathVariable UUID uuid) { public ResponseEntity<CustomerResponseDto> getCustomer(@PathVariable UUID uuid) {
// 권한 체크 // 권한 체크
PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (!PermissionChecker.canReadCRM(permissionSet)) {
throw new AccessDeniedException("You do not have permission to read CRM data"); 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)); return ResponseEntity.ok(customerService.getCustomerByUuid(uuid));
} }
@ -66,10 +104,16 @@ public class CustomerController {
@PathVariable UUID uuid, @PathVariable UUID uuid,
@RequestBody CustomerRequestDto requestDto) { @RequestBody CustomerRequestDto requestDto) {
// 권한 체크 // 권한 체크
PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (!PermissionChecker.canUpdateCRM(permissionSet)) {
throw new AccessDeniedException("You do not have permission to update CRM data"); 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)); return ResponseEntity.ok(customerService.updateCustomer(uuid, requestDto));
} }
@ -78,10 +122,16 @@ public class CustomerController {
@DeleteMapping("/{uuid}") @DeleteMapping("/{uuid}")
public ResponseEntity<Void> deleteCustomer(@PathVariable UUID uuid) { public ResponseEntity<Void> deleteCustomer(@PathVariable UUID uuid) {
// 권한 체크 // 권한 체크
PermissionSet permissionSet = (PermissionSet) SecurityContextHolder.getContext().getAuthentication().getDetails(); PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
if (!PermissionChecker.canDeleteCRM(permissionSet)) {
throw new AccessDeniedException("You do not have permission to delete CRM data"); 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); customerService.deleteCustomer(uuid);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();

View File

@ -1,7 +1,12 @@
package com.goi.erp.repository; package com.goi.erp.repository;
import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.entity.Customer; 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.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional; import java.util.Optional;
@ -9,5 +14,21 @@ import java.util.UUID;
@Repository @Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> { public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> 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<CustomerResponseDto> 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<CustomerResponseDto> findAllCustomerDtos(Pageable pageable);
Optional<Customer> findByCusUuid(UUID cusUuid);
} }

View File

@ -5,11 +5,13 @@ import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.entity.Customer; import com.goi.erp.entity.Customer;
import com.goi.erp.repository.CustomerRepository; import com.goi.erp.repository.CustomerRepository;
import lombok.RequiredArgsConstructor; 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 org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -29,19 +31,17 @@ public class CustomerService {
.cusProvince(dto.getCusProvince()) .cusProvince(dto.getCusProvince())
.build(); .build();
customer = customerRepository.save(customer); customer = customerRepository.save(customer);
return mapToDto(customer); return mapToDto(customer); // 생성 시에는 여전히 엔티티 DTO 필요
} }
public List<CustomerResponseDto> getAllCustomers() { public Page<CustomerResponseDto> getAllCustomers(int page, int size) {
return customerRepository.findAll().stream() Pageable pageable = PageRequest.of(page, size);
.map(this::mapToDto) return customerRepository.findAllCustomerDtos(pageable); // DTO 바로 조회
.collect(Collectors.toList());
} }
public CustomerResponseDto getCustomerByUuid(UUID uuid) { public CustomerResponseDto getCustomerByUuid(UUID uuid) {
Customer customer = customerRepository.findByCusUuid(uuid) return customerRepository.findCustomerDtoByCusUuid(uuid)
.orElseThrow(() -> new RuntimeException("Customer not found")); .orElseThrow(() -> new RuntimeException("Customer not found"));
return mapToDto(customer);
} }
public CustomerResponseDto updateCustomer(UUID uuid, CustomerRequestDto dto) { public CustomerResponseDto updateCustomer(UUID uuid, CustomerRequestDto dto) {

View File

@ -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<? extends GrantedAuthority> authorities) {
super(principal, null, authorities);
this.permissionSet = permissionSet;
}
public PermissionSet getPermissionSet() {
return permissionSet;
}
}

View File

@ -22,6 +22,10 @@ application:
expiration: 86400000 # a day expiration: 86400000 # a day
refresh-token: refresh-token:
expiration: 604800000 # 7 days expiration: 604800000 # 7 days
pagination:
default-page: 0
default-size: 20
max-size: 100
server: server:
port: 8082 port: 8082
servlet: servlet: