코드 그라데이션
JWT 예제 구현 (9) Exception 세분화, 리다이렉트 수정, Response 로직 변경, 리다이렉트 테스트 추가 본문
Spring/Security
JWT 예제 구현 (9) Exception 세분화, 리다이렉트 수정, Response 로직 변경, 리다이렉트 테스트 추가
완벽한 장면 2023. 12. 18. 12:12DTO
ErrorDto
package inflearn.freejwt.dto;
import org.springframework.validation.FieldError;
import java.util.ArrayList;
import java.util.List;
// ErrorDTO 클래스는 API 또는 웹 애플리케이션에서 예외 또는 오류 정보를 표현하는 데 사용됩니다.
public class ErrorDto {
// 상태 코드를 나타내는 변수. HTTP 상태 코드와 관련이 있으며, 클라이언트에게 반환됨.
private final int status;
// 클라이언트에게 오류의 원인을 설명하는 데 사용.
private final String message;
// 필드 오류(FieldError) 객체들을 저장하는 리스트.
// 필드 오류는 양식 유효성 검사와 관련이 있으며, 특정 필드에 대한 오류를 저장.
private List<FieldError> fieldErrors = new ArrayList<>();
// ErrorDTO 클래스의 생성자. 상태 코드와 메시지를 전달하여 객체를 초기화.
public ErrorDto(int status, String message) {
this.status = status;
this.message = message;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
// 필드 오류(FieldError)를 추가하는 메서드입
// 이 메서드를 사용하여 특정 필드에 대한 오류를 ErrorDTO 객체에 추가할 수 있다.
public void addFieldError(String objectName, String path, String message) {
FieldError error = new FieldError(objectName, path, message);
fieldErrors.add(error);
}
public List<FieldError> getFieldErrors() {
return fieldErrors;
}
}
Exception
DuplicateMemberException
// 사용자 정의 예외 클래스인 DuplicateMemberException 을 정의.
public class DuplicateMemberException extends RuntimeException {
// 기본 생성자
public DuplicateMemberException() {
super();
}
// 메시지와 원인 예외를 받는 생성자
public DuplicateMemberException(String message, Throwable cause) {
super(message, cause);
}
// 메시지를 받는 생성자
public DuplicateMemberException(String message) {
super(message);
}
// 원인 예외를 받는 생성자
public DuplicateMemberException(Throwable cause) {
super(cause);
}
}
Service
UserService 수정
package inflearn.freejwt.service;
import inflearn.freejwt.dto.UserDto;
import inflearn.freejwt.entity.Authority;
import inflearn.freejwt.entity.User;
import inflearn.freejwt.exception.DuplicateMemberException;
import inflearn.freejwt.repository.UserRepository;
import inflearn.freejwt.util.SecurityUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public User signup(UserDto userDto) {
if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
throw new DuplicateMemberException("이미 가입되어 있는 유저입니다."); // 변경
}
// 권한 정보 생성
Authority authority = Authority.builder()
.authorityName("ROLE_USER")
.build();
// 유저 정보 생성
User user = User.builder()
.username(userDto.getUsername())
.password(passwordEncoder.encode(userDto.getPassword()))
.nickname(userDto.getNickname())
.authorities(Collections.singleton(authority))
.activated(true)
.build();
// 저장
return userRepository.save(user);
}
/**
*
* @param username
* @return
* username을 기준으로 정보를 가져오는 메서드
*/
@Transactional(readOnly = true)
public Optional<User> getUserWithAuthorities(String username) {
return userRepository.findOneWithAuthoritiesByUsername(username);
}
/**
* SecurityContext에 저장된 username의 정보만 가져온다.
* @return
*/
@Transactional(readOnly = true)
public Optional<User> getMyUserWithAuthorities() {
return SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername);
}
}
application.yml 추가
spring:
h2:
console:
enabled: true
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
properties:
hibernate:
format_sql: true
show_sql: true
defer-datasource-initialization: true # 추가
jwt:
header: Authorization
#HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
#echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
logging:
level:
inflearn.freejwt: DEBUG
handler
RestResponseEntityException 생성
package inflearn.freejwt.handler;
import inflearn.freejwt.dto.ErrorDto;
import inflearn.freejwt.exception.DuplicateMemberException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
// @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스임을 나타냅니다.
@ControllerAdvice
public class RestResponseEntityException extends ResponseEntityExceptionHandler {
// @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정.
@ResponseStatus(HttpStatus.CONFLICT)
// @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정.
@ExceptionHandler(value = { DuplicateMemberException.class })
// @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함.
@ResponseBody
// ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드.
// 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
protected ErrorDto badRequest(RuntimeException ex, WebRequest request) {
// ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
}
}
MethodArgumentNotValidException 수정
package inflearn.freejwt.handler;
import inflearn.freejwt.dto.ErrorDto;
import inflearn.freejwt.exception.DuplicateMemberException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import java.util.List;
// 이 어노테이션은 클래스가 예외 처리기로 사용될 때 우선순위를 지정.
// Ordered.HIGHEST_PRECEDENCE는 가장 높은 우선순위를 나타내며,
// 이 클래스가 다른 예외 처리기보다 먼저 실행됨을 의미.
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
// @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스임을 알려준다.
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
// @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정한다.
@ResponseStatus(HttpStatus.CONFLICT)
// @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정한다.
@ExceptionHandler(value = { DuplicateMemberException.class })
// @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함을 나타낸다.
@ResponseBody
// ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드입니다.
// 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
protected ErrorDto badRequest(RuntimeException ex, WebRequest request) {
// ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
}
}
}
고치기 전 코드는 이거였다
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public Error methodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<org.springframework.validation.FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private Error processFieldErrors(List<org.springframework.validation.FieldError> fieldErrors) {
Error error = new Error(HttpStatus.BAD_REQUEST.value(), "@Valid Error");
for (org.springframework.validation.FieldError fieldError : fieldErrors) {
error.addFieldError(fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
}
return error;
}
// 오류 응답을 나타내는 내부 클래스.
static class Error {
private final int status;
private final String message;
private List<FieldError> fieldErrors = new ArrayList<>();
// 오류 객체를 생성할 때 HTTP 응답 상태 코드와 메시지를 설정.
Error(int status, String message) {
this.status = status;
this.message = message;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
public void addFieldError(String objectName, String path, String message) {
FieldError error = new FieldError(objectName, path, message);
fieldErrors.add(error);
}
public List<FieldError> getFieldErrors() {
return fieldErrors;
}
}
}
테스트 집어넣
controller
UserController 수정
package inflearn.freejwt.controller;
import inflearn.freejwt.dto.UserDto;
import inflearn.freejwt.entity.User;
import inflearn.freejwt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
/**
* 사용자 회원가입을 처리하는 엔드포인트.
*
* @param userdto 회원가입 요청에 필요한 사용자 정보를 담은 DTO 객체
* @return 회원가입이 성공하면 사용자 정보를 담은 ResponseEntity를 반환
*/
@PostMapping("/signup")
public ResponseEntity<User> siginup(@Valid @RequestBody UserDto userdto) {
return ResponseEntity.ok(userService.signup(userdto));
}
/**
* 현재 사용자의 정보를 조회하는 엔드포인트.
* 사용자 및 관리자 역할을 가진 사용자만 접근할 수 있다.
*
* @return 현재 사용자의 정보를 담은 ResponseEntity를 반환
*/
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public ResponseEntity<User> getMyUserInfo() {
// userService를 사용하여 현재 사용자의 정보를 조회하고, 그 결과를 ResponseEntity로 반환.
// getMyUserWithAuthorities()는 현재 사용자의 정보와 권한 정보를 함께 조회하는 서비스 메서드.
// 반환된 정보는 ResponseEntity.ok()를 사용하여 HTTP 200 OK 상태와 함께 반환됨.
return ResponseEntity.ok(userService.getMyUserWithAuthorities().get());
}
// 추가
@PostMapping("/test-redirect")
public void testRedirect(HttpServletResponse response) throws IOException {
response.sendRedirect("/api/user");
}
/**
* 특정 사용자의 정보를 조회하는 엔드포인트.
* 관리자 역할을 가진 사용자만 접근할 수 있다.
*
* @param username 조회할 사용자의 이름
* @return 특정 사용자의 정보를 담은 ResponseEntity를 반환
*/
@GetMapping("/user/{username}")
@PreAuthorize("hasAnyRole('ADMIN')")
// 요기
public ResponseEntity<User> getMyUserInfo(HttpServletRequest request) {
// System.out.println(request.getHeader("Authorization"));
// userService를 사용하여 특정 사용자의 정보를 조회하고, 그 결과를 ResponseEntity로 반환함.
// getUserWithAuthorities(username)는 특정 사용자 정보와 권한 정보를 함께 조회하는 서비스 메서드.
// 반환된 정보는 ResponseEntity.ok()를 사용하여 HTTP 200 OK 상태와 함께 반환됨.
return ResponseEntity.ok(userService.getMyUserWithAuthorities().get());
}
}
728x90
'Spring > Security' 카테고리의 다른 글
[중간점검] 또 여기까지 전체 코드 정리 / 버전 2.5.3 (0) | 2023.12.20 |
---|---|
JWT 예제 구현 (10) Response 시 DTO를 통해서만 응답하도록 변경 (0) | 2023.12.19 |
JWT 예제 구현 (8) 예외처리 로직 추가, Exception(핸들러) 추가, 버전 2.53 업그레이드 등 (0) | 2023.12.16 |
[중간점검] JWT 2.41 버전 구현 로직 완성 코드 (0) | 2023.12.15 |
JWT 예제 구현 (7) 회원가입, 권한 검증 로직 (0) | 2023.12.13 |
Comments