코드 그라데이션

JWT 예제 구현 (9) Exception 세분화, 리다이렉트 수정, Response 로직 변경, 리다이렉트 테스트 추가 본문

Spring/Security

JWT 예제 구현 (9) Exception 세분화, 리다이렉트 수정, Response 로직 변경, 리다이렉트 테스트 추가

완벽한 장면 2023. 12. 18. 12:12

DTO

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
Comments