SPRING&BOOT

Spring Boot에서 JWT 인증 필터와 ArgumentResolver 활용하기

jki09871 2024. 9. 20. 20:07
이번 포스팅에서는 JWT 인증 필터를 설정하고, 이를 통해 인증된 사용자 정보를 컨트롤러로 전달하기 위한 HandlerMethodArgumentResolver 구현 방법을 소개하겠다. Spring Security 없이 커스텀 JWT 필터를 통해 권한과 사용자를 처리하는 구조를 간단하게 설명할 것이다.

 

1. JWT 인증 필터 (JwtFilter)

먼저, 사용자의 요청을 처리하기 전에 JWT 토큰을 검증하는 JwtFilter를 작성하겠다.

코드 분석

@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 로직 (필요한 경우에만 구현)
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 요청과 응답 객체를 HttpServletRequest, HttpServletResponse 로 캐스팅
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 현재 요청된 URL을 가져옴
        String url = httpRequest.getRequestURI();

        // 회원가입, 로그인 페이지는 필터를 적용하지 않음
        if (url.equals("/api/users/signup") || url.equals("/api/users/login")) {
            chain.doFilter(request, response); // 필터를 거치지 않고 다음 단계로 진행
            return;
        }

        // Authorization 헤더에서 JWT 토큰을 가져옴
        String tokenValue = httpRequest.getHeader("Authorization");

        // 토큰이 없거나 Bearer 로 시작하지 않으면 에러 응답
        if (tokenValue == null || !tokenValue.startsWith("Bearer ")) {
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰이 필요합니다.");
            return;
        }

        // Bearer 이후의 실제 토큰 값만 추출
        String token = jwtUtil.substringToken(tokenValue);

        try {
            // 토큰에서 사용자 정보를 추출
            Claims claims = jwtUtil.getUserInfoFromToken(token);

            // role 정보를 Enum 타입으로 변환
            String roleString = claims.get("role", String.class);
            UserRoleEnum role = UserRoleEnum.valueOf(roleString);

            // HttpServletRequest 에 사용자 정보를 세팅하여 나중에 접근할 수 있도록 함
            httpRequest.setAttribute("role", role);
            httpRequest.setAttribute("email", claims.get("email", String.class));

            // 토큰 유효성을 검사하고 유효하면 필터를 통과
            if (jwtUtil.validateToken(token)) {
                chain.doFilter(request, response);
            } else {
                // 토큰이 유효하지 않으면 에러 응답
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰이 유효하지 않습니다.");
            }
        } catch (Exception e) {
            // 토큰 검증 중 오류 발생 시 에러 응답
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰 검증 중 오류가 발생했습니다.");
        }
    }

    @Override
    public void destroy() {
        // 필터 종료 시 로직 (필요한 경우에만 구현)
        Filter.super.destroy();
    }
}

설명

  • JwtFilter는 HTTP 요청에서 Authorization 헤더를 통해 JWT 토큰을 추출하고, 이를 검증하여 유효한지 확인한다.
  • 토큰이 유효하면 HttpServletRequest에 사용자 정보(email, role)를 저장하고 다음 필터로 넘겨준다.
  • 회원가입과 로그인, 그리고 홈 페이지는 필터를 통과하도록 설정했다.

2. 사용자 인증 정보를 ArgumentResolver로 전달하기

JWT 필터를 통해 설정된 사용자 정보는 컨트롤러에서 쉽게 활용할 수 있도록 HandlerMethodArgumentResolver로 전달한다. 이 부분에서는 사용자 인증 정보가 필요한 컨트롤러 메서드에 AuthUser 객체를 주입하는 방법을 알아보겠다.

Auth 어노테이션

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}

AuthUserArgumentResolver 구현

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {

    // @Auth 어노테이션이 있는지 확인
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 파라미터에 @Auth 어노테이션이 있는지 확인
        boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
        // 파라미터 타입이 AuthUser 클래스인지 확인
        boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);

        // @Auth 어노테이션과 AuthUser 타입은 함께 사용되어야 함
        if (hasAuthAnnotation != isAuthUserType) {
            throw new IllegalArgumentException("@Auth 와 AuthUser 타입은 함께 사용되어야 합니다.");
        }

        return isAuthUserType; // 파라미터가 AuthUser 타입이면 true 반환
    }

    // AuthUser 객체를 생성하여 반환
    @Override
    public Object resolveArgument(
            @Nullable MethodParameter parameter,
            @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            @Nullable WebDataBinderFactory binderFactory
    ) {
        // HttpServletRequest 에서 필터로 세팅한 사용자 정보를 가져옴
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        // JwtFilter 에서 set 한 email 값을 가져옴
        String email = (String) request.getAttribute("email");
        // JwtFilter 에서 set 한 role 값을 가져옴
        UserRoleEnum role = (UserRoleEnum) request.getAttribute("role");

        // AuthUser 객체를 생성하여 반환
        return new AuthUser(email, role);
    }
}

설명

  • supportsParameter 메서드에서는 파라미터가 @Auth 어노테이션을 가졌는지, 그리고 AuthUser 타입인지 확인한다.
  • resolveArgument 메서드에서는 HttpServletRequest에서 필터에서 설정한 email과 role을 꺼내 AuthUser 객체로 반환한다.

3. ArgumentResolver 등록

WebConfig를 통해 이 AuthUserArgumentResolver를 Spring MVC에 등록해주어야 한다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    // ArgumentResolver 등록
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        // AuthUserArgumentResolver 를 추가하여 컨트롤러에서 사용 가능하게 함
        resolvers.add(new AuthUserArgumentResolver());
    }
}

4. JWT 필터를 Spring에 등록하기

마지막으로, JWT 필터를 Spring에서 사용할 수 있도록 FilterConfig를 통해 등록해준다.

@Configuration
@RequiredArgsConstructor
public class FilterConfig {
    private final JwtUtil jwtUtil;

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        // JWT 필터를 등록하는 설정
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter(jwtUtil)); // JwtFilter 를 필터로 등록
        registrationBean.addUrlPatterns("/*"); // 모든 URL 패턴에 필터를 적용

        return registrationBean;
    }
}

결론

이번 포스팅에서는 Spring Boot에서 JWT 인증을 처리하기 위해 JwtFilter를 구현하고, 사용자 정보를 HandlerMethodArgumentResolver를 통해 컨트롤러로 전달하는 방법을 설명했다. Spring Security를 사용하지 않고도 JWT 기반의 인증을 간단하게 구현할 수 있는 방법이다. 이 구조를 활용하면 커스텀한 인증 로직을 쉽게 확장하고, 유지보수하기도 좋다.

'SPRING&BOOT' 카테고리의 다른 글

Hibernate의 @DynamicInsert와 @DynamicUpdate 정리  (1) 2024.09.30
PUT과 PATCH 메서드  (1) 2024.09.27
AOP(Aspect-Oriented Programming) 개념 및 실습  (0) 2024.09.10
필터  (0) 2024.09.03
캡슐화(Encapsulation)  (0) 2024.08.28