JPA - JWT와 필터 구현하기

2024. 10. 16. 20:27·TIL

JwtUtil

이번 과제에서는 JWT 토큰을 이용한 인증, 인가를 구현해야한다. 

그렇기 때문에 JWT 토큰 생성만을 위한 Util 클래스를 만들었다.

@Component
public class JwtUtil {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String AUTHORIZATION_KEY = "auth";
    public static final String BEARER_PREFIX = "Bearer ";
    private final long ACCESS_TOKEN_TIME = 60 * 60 * 1000L; // 60분

	
    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

    @PostConstruct
    public void init(){
        byte[] keyBytes = secretKey.getBytes();
        key = Keys.hmacShaKeyFor(keyBytes);
    }

    public String createAccessToken(Long id, String role){
        Date date = new Date();

        Claims claims = Jwts.claims();

        claims.put("id", id);
        claims.put(AUTHORIZATION_KEY, role);

        return BEARER_PREFIX+
                Jwts.builder()
                        .setClaims(claims)
                        .setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key,signatureAlgorithm)
                        .compact();
    }

    public void addJwtToHeader(HttpServletResponse response, String token){
            logger.info("토큰 추가");
        try {
            token = URLEncoder.encode(token, "UTF-8").replaceAll("\\+", "%20");
            response.setHeader(AUTHORIZATION_HEADER,token);
        } catch (UnsupportedEncodingException e) {
            throw new CustomException(ErrorCode.INVALID_ENCODE);
        }
    }

    public Claims getUserInfo(String token){
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    public String getTokenFromRequest(HttpServletRequest request) throws UnsupportedEncodingException {
        String token = request.getHeader(AUTHORIZATION_HEADER);

        token = URLDecoder.decode(token,"UTF-8");

        if (!(StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX))) throw  new CustomException(ErrorCode.NULL_TOKEN);
        return token.substring(BEARER_PREFIX.length());
    }
}

 

LogFilter

가장 먼저 지나치는 필터로 다음 필터에서 일어나는 예외들을 잡아서 던져준다.

@Slf4j
@Component
@Order(0)
public class LogFilter extends OncePerRequestFilter{

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            log.info("로그 필터 시작");
            filterChain.doFilter(request, response);
        }
        catch (SecurityException | MalformedJwtException | SignatureException e) {
            setErrorResponse(response, ErrorCode.INVALID_TOKEN);
        } catch (ExpiredJwtException  e) {
            setErrorResponse(response, ErrorCode.NOT_VALID_TOKEN);
        } catch (UnsupportedJwtException  e) {
            setErrorResponse(response, ErrorCode.UNSUPPORTED_TOKEN);
        } catch (IllegalArgumentException | NullPointerException e) {
            setErrorResponse(response,ErrorCode.NULL_TOKEN);
        }
        catch (CustomException e){
            setErrorResponse(response,e.getErrorCode());
        }
        log.info("로그 필터 종료");
    }

    private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) {
        response.setStatus(errorCode.getHttpStatus());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        try {
            String json = new ObjectMapper().writeValueAsString(new ErrorMessageResponseDto(errorCode));
            response.getWriter().write(json);
        } catch(Exception e) {
            response.getStatus();
        }
    }
}

 

TokenFilter

전반적인 Token 검증 및 유저 권한을 확인하는 필터이다.

@Slf4j
@Component
@Order(1)
@RequiredArgsConstructor
public class TokenFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;
    private final UserRepository repository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("토큰 필터 시작");
        String uri = request.getRequestURI();
        // 로그인과 회원가입 요청은 토큰 인증처리 제외
        if(uri.startsWith("/api/user/join") || uri.startsWith("/api/user/login")) filterChain.doFilter(request, response);

        String token = jwtUtil.getTokenFromRequest(request);
		
        // 들어온 요청이 일정 수정 및 삭제라면 Token에서 유저 권한정보를 꺼내어서
        // 관리자 권한 유저인지 확인 후 없다면 예외처리 관리자 권한이 있는 유저라면
        // 계속 진행
        if(uri.startsWith("/api/todo/modify")|| uri.startsWith("/api/todo/delete")){
            if(!jwtUtil.getUserInfo(token).get("auth").equals(UserRole.ADMIN.getAuthority()))throw new CustomException(ErrorCode.NOT_ADMIN);
        }

        Long id = jwtUtil.getUserInfo(token).get("id", Long.class);
        User user = repository.findById(id).orElseThrow(()-> new CustomException(ErrorCode.NOT_USER_ID));

        request.setAttribute("user", user);
        filterChain.doFilter(request, response);
        log.info("토큰 필터 종료");
    }
}

 

트러블 슈팅

서로 다른 예외코드를 던져줘야하는데 같은 Exception를 일으키는 부분들이 있어서 이 부분을 어떻게 할지 고민하며 

방법을 찾던 중 CustomException클래스를 따로 만들어서 관리를 할 수 있다는걸 알게되어서 CustomException 클래스를따로 만들고,  각각의 예외마다 알맞은 msg를 전해주기 위해 ErrorCode enum을 생성하여서 상태코드와 메세지를

각 상황에 맞게 보내줄 수 있도록 하였다.  지금까지는 그냥 Exception만 던져줬었는데 이번에 직접 ErrorCode enum과

커스텀 입섹션 클래스들을 만들어서 각각의 상황에 맞는 상태코드와 메세지를 던져주니 포스트맨으로 테스트하며

과제 요구사항들을 구현할 때 정말 도움이 많이 되었다. 앞으로도 예외처리는 이번에 했던 방법을 정말 잘 이용할 것 같다.

@Getter
public enum ErrorCode {
    NOT_VALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효기간 만료된 토큰"),
    UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED,"지원하지 않는 토큰"),
    NOT_MATCH_LOGIN(HttpStatus.UNAUTHORIZED,"이메일과 비밀번호 틀림"),
    EMAIL_DUPLICATION(HttpStatus.UNAUTHORIZED,"중복된 이메일입니다."),
    NOT_MANAGER(HttpStatus.UNAUTHORIZED,"담당 유저로 등록되어 있지 않습니다."),
    NULL_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 존재하지 않습니다."),
    NO_API_DATA(HttpStatus.BAD_REQUEST,"api 데이터가 존재하지 않습니다."),
    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 JWT 서명."),
    INVALID_ENCODE(HttpStatus.BAD_REQUEST, "인코딩 에러"),
    NOT_TODO_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 TODO ID"),
    NO_MANAGER_MY(HttpStatus.BAD_REQUEST,"본인이 쓴 글에 본인을 담당 유저로 배치할 수 없습니다."),
    NO_MY_WRITE_TODO(HttpStatus.BAD_REQUEST,"타인의 일정에 다른 유저를 담당유저로 배치할 수 없습니다."),
    NOT_COMMENT_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 Comment ID"),
    NOT_USER_ID(HttpStatus.BAD_REQUEST,"존재하지 않는 USER ID"),
    DIFFERENT_USER(HttpStatus.FORBIDDEN,"본인 계정만 삭제 및 수정할 수 있습니다."),
    NO_MY_WRITE_COMMENT(HttpStatus.FORBIDDEN,"본인이 쓴 댓글만 삭제 및 수정 할 수 있습니다."),
    MANAGER_DUPLICATION(HttpStatus.FORBIDDEN,"이미 담당 유저로 배치 되어 있는 일정입니다."),
    NOT_ADMIN(HttpStatus.FORBIDDEN,"권한이 없습니다.");

    private final int httpStatus;
    private final String message;

	//httpStatus  = 현재 httpstatus 코드를 전달해준다.
	//message   =  해당 에러가 왜 일어났는지 알 수 있도록 도와주도록 메세지를 전달해주는 역할
    ErrorCode(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus.value();
        this.message = message;
    }
}

 

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
	// 발생한 예외 상황에 대해 전달하기 위한 데이터
    ErrorCode errorCode;
}


@ControllerAdvice
@Slf4j
public class CustomExceptionHandler {

	// CustomException 발생 시 프런트로 보낼 ErrorMessageResponseDto를 생성 및 전달 해주는 클래스
    @ExceptionHandler(value = CustomException.class)
    public ResponseEntity<ErrorMessageResponseDto> CustomException(CustomException e) {
        return returnResponse(e.getErrorCode());
    }

    private ResponseEntity<ErrorMessageResponseDto> returnResponse(ErrorCode errorCode) {
        ErrorMessageResponseDto responseDto = new ErrorMessageResponseDto(errorCode);
        return ResponseEntity.status(responseDto.getErrorCode()).body(responseDto);
    }
}

@Getter
public class ErrorMessageResponseDto {
    private int errorCode;
    private String message;
	
    // 데이터 전달 예시
    // errCode : HttpStatus.FORBIDDEN
    // message : "권한이 없습니다"
    public ErrorMessageResponseDto(ErrorCode errorCode) {
        this.errorCode = errorCode.getHttpStatus();
        this.message = errorCode.getMessage();
    }
}

회고

Jwt토큰과 필터를 이번에 처음 사용하다보니 강의에서 예제로 쓴 코드들을 분석하는 시간이 많았었던 것 같다.

처음에는 이게 어떻게 흘러가는 것인지 흐름조차 느낌이 오지 않았지만 계속 들여다보고 처음보는 메서드같은건 검색을하며 이래서 쓰는거구나 하고 직접 내 과제에도 적용해보면서 이제는  어느정도 흘러가는 느낌은 알게된 것 같다. 

처음에는 TokenFilter 클래스의 doFilter 내부에 if문들이 많았었는데 리팩토링을 하다보니 쓸데없는 것들이 너무 많은것을 보며 코드를 다짜고 리팩토링은 꼭 해야겠구나 하는 생각이 들었다.

'TIL' 카테고리의 다른 글

JPA - 일정관리 앱 만들기  (1) 2024.10.17
JPA - 회원가입 로그인  (0) 2024.10.17
영속성 컨텍스트  (1) 2024.10.16
페이징 조회 - Pageble / PageRequest  (0) 2024.10.15
JPA - Entity  (1) 2024.10.15
'TIL' 카테고리의 다른 글
  • JPA - 일정관리 앱 만들기
  • JPA - 회원가입 로그인
  • 영속성 컨텍스트
  • 페이징 조회 - Pageble / PageRequest
haseung22
haseung22
haseung22 의 블로그 입니다.
  • haseung22
    haseung22의 블로그
    haseung22
  • 전체
    오늘
    어제
    • 분류 전체보기 (56)
      • TIL (39)
      • 프로그래머스 (8)
      • 프로젝트 (5)
      • 면접 대비 (4)
        • 자료구조 (3)
        • Java (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    docker
    뉴스피드
    회고
    과제
    java
    Spring
    팀 프로젝트
    querydsl
    Spring Boot
    오블완
    자료구조
    JPA
    til
    공부
    리팩토링
    계산기
    내배캠
    알고리즘
    프로그래머스
    티스토리챌린지
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
haseung22
JPA - JWT와 필터 구현하기
상단으로

티스토리툴바