그라찌에 트러블 이슈 Interceptor -> Filter

류희수·2024년 9월 28일
post-thumbnail

무작정 request Interceptor로 구현하였는데..
이거 매번 컨트롤러마다 @AuthenticationPrincipal 를 붙여야하는 불상사가 일어나고 있었다...

사실 내가 체크하려는 로직은 로그인여부와 같이 간단한 로직이니 굳이 비지니스 로직에서 검사하는 것이 아닌 스프링 전역에서 유지하는 Filter 를 사용하는 것이 더 좋을 것 같다는 생각이 들었다..
(아니 근데 사실 그러면 request interceptor는 언제 쓰지...?

즉 로그인 여부는 스프링과 관련이 없으니... HTTP 선에서 처리하는 것이 더 깔끔할 것 같았다..

내가 생각한 것은

  1. 클라이언트가 서버로 HTTP 요청을 보낸다.
  2. Filter가 요청을 가로채서 요청을 처리하거나 검증한다!!
  3. 필터가 자신의 역할을 마치고, 요청을 다음 단계(다음 필터 또는 서블릿)로 넘기기 위해 chain.doFilter(request, response)를 호출한다.
  4. 다음 필터가 없으면 서블릿이나 컨트롤러가 요청을 처리한다!

아무튼 그래서 Interceptor -> Filter 로 변경해보자.....

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {

    private final JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String token = resolveToken(httpRequest);

        if (token != null) {
            try {
                jwtUtil.extractAllClaims(token); // 토큰 검증
                Authentication authentication = jwtUtil.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication); // context 홀더에 넣기
            } catch (RuntimeException e) {
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다: " + e.getMessage());
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
            if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

filter 클래스 구현!
interceptor 와 사실상 거의 다 비슷하다.

public class SecurityUtils {

    public static UserAdapter getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return (UserAdapter) authentication.getPrincipal();
        }
        throw new UnauthorizedException("인증되지 않은 사용자입니다.");
    }
}

디테일 하게 하기로 한김에 adapter에서 현재 유저를 찾아올 수 있는 getCurrentUser를 일단 생성 추후에 더 필요한 것이 있으면 생성하면 될 것 같다!

public enum ErrorMessage {
    UNAUTHORIZED("인증되지 않은 사용자입니다."),
    USER_NOT_FOUND("사용자가 존재하지 않습니다."),
    BAD_REQUEST("잘못된 요청입니다.");

    private final String message;

    ErrorMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Enum으로 ErrorMSG도 간단하게 만들었는데 이건 나중에 회의 후 수정하도록 하겠다.

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException() {
        super(ErrorMessage.UNAUTHORIZED.getMessage());
    }
}

UnauthorizedException
인증되지 않은 예외에 대해 메시지를 던져주기
하지만 보면 알겠지만 무수한 try/ catch늪에 빠져있다.
그래서 어처피 공통된 예외이니 글로벌 익셉션 핸들러를 사용해서 전역적으로 예외를 처리하도록 하겠다
아~ 관점지향적이다~

@ControllerAdvice

public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("오류가 발생했습니다: " + e.getMessage());
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
        e.printStackTrace();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorMessage.BAD_REQUEST.getMessage() + ": " + e.getMessage());
    }

    @ExceptionHandler(UnauthorizedException.class)
    public ResponseEntity<String> handleUnauthorizedException(UnauthorizedException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(e.getMessage());
    }
    
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<String> handleAccessDeniedException(AccessDeniedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("접근이 거부되었습니다: " + e.getMessage());
    }
}

간단하게 큼지막한 에러들만 모아두었다!
오.. 이제 진짜 뭔가 개발하는 거 같아서 재미가 들리기 시작했다!! 추후에 에러메시지들도 추가해서 수정하도록 하겠다!!

@Controller
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService userService;
    private final PasswordTokenRepository tokenRepository;
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    @PostMapping("/join")
    public ResponseEntity<User> join(@RequestBody User user) {
        try {
            User savedUser = userService.joinUser(user);
            return ResponseEntity.ok(savedUser);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    @GetMapping("/readProfile")
    public ResponseEntity<?> readUserProfile() {
        UserAdapter userAdapter = SecurityUtils.getCurrentUser();
        if (userAdapter == null) {
            throw new UnauthorizedException(); 
        }

        UserDTO userDTO = userService.readUser(userAdapter);
        return ResponseEntity.ok(userDTO);
    }


    @PutMapping("/update")
    public ResponseEntity<?> updateProfile(@RequestBody UserDTO userDTO) {
        UserAdapter userAdapter = SecurityUtils.getCurrentUser();
        if (userAdapter == null) {
            throw new UnauthorizedException();
        }

        User user = userService.updateUser(userAdapter, userDTO);
        return ResponseEntity.ok(user);
    }


    @DeleteMapping("/delete")
    public ResponseEntity<?> deleteUser() {
        UserAdapter userAdapter = SecurityUtils.getCurrentUser();
        if (userAdapter == null) {
            throw new UnauthorizedException(); 
        }

        userService.deleteUser(userAdapter);
        return ResponseEntity.ok().build();
    }

    @PutMapping("/changeTempPassword")
    public ResponseEntity<?> verifyTempPassword(@RequestParam("token") String token,
                                                @RequestBody PasswordDTO passwordDTO) {
        PasswordToken passwordToken = tokenRepository.findByToken(token)
                .orElseThrow(() -> new RuntimeException("유효하지 않은 토큰임"));

        if (passwordToken.getExpiryDate().isBefore(LocalDateTime.now())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("토큰이 만료되었음");
        }

        User user = passwordToken.getUser();
        String newPassword = passwordDTO.getNewPassword();
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
        tokenRepository.delete(passwordToken);

        return ResponseEntity.ok("비밀번호 변경 ok");
    }

    @PutMapping("/changePassword")
    public ResponseEntity<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
        UserAdapter userAdapter = SecurityUtils.getCurrentUser();
        if (userAdapter == null) {
            throw new UnauthorizedException(); 
        }

        userService.updatePassword(userAdapter, passwordDTO);
        return ResponseEntity.ok("비밀번호가 변경되었습니다.");
    }
}

하아.. 아름답다 아름다워...
확실히 코드가 우아?하게 줄었다!!
아직 예외처리 안한 것들도 많기는 한데 그건 다음 편에서 전부 예외를 잡아 볼 생각이다!! 일단은 filter 적용부터~

이제

@AuthenticationPrincipal UserAdapter userAdapter 로 가져오는 것과
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
로 수동적으로 가져오는 것중 어떤 것을 택해야 할지 고민하였다.
가독성 부분과 편리함 부분에서는 위에 어노테이션이 편하긴 한데,,, 후자는 조금 더 유연한 처리를 할 수가 있다.
이미 userDeatails를 구현하기도 하였고, 무작정 자동화보다는 조금 더 유연하게 에러를 한번 잡아보고 싶어서 수동적으로 하는 방법을 선택하였다.


에러

에러가 없으면 섭하지
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 0
자꾸 jwt토큰을 못 찾아오고 있었다.

흠.. 에러 메시지를 보면 .으로 구별된것을 못찾고 있는 것 같은데...

wow... 내가 초반에 개발한다고 타입을 token으로 직접 주입받고 있어서 당연히 토큰 넣는데다가 userAdpater를 냅다 집어넣고 있었다... 이 간단한걸!!!!!
그래도 점점 디버깅하는 실력이 조금씩 늘고 있다!!

profile
자바를자바

0개의 댓글