[Spring][Security] 인증 과정, OncePerRequestFilter를 사용하는 이유

jhkim·2023년 10월 2일
1
post-thumbnail

스프링 시큐리티 아키텍처

  • SecutiryContextHolder

    • SecurityContext 객체를 저장하고 감싸고 있는 wrapper 클래스
    • getContext() 전역 메소드를 제공하여 securityContext에 접근이 가능함
    • 기본적으로 THREADLOCAL
  • SecurityContext

    • Authentication을 저장하고 있음
    • Threadlocal에 젖아되어 어디서든 참조가 가능함
  • Authentication

    • Principal과 GrantAuthority를 제공
    • 대부분의 경우 Principal로 UserDetails 반환함. 이 정보는 @AuthenticationPrincipal 어노테이션을 통해 컨트롤러에서 사용 가능하며, 필요할 경우 UserDetails를 상속받아 커스텀하여 추가 정보 저장
  • GrantAuthority

    • ROLE_ADMIN, ROLE_USER등 Principal이 가지고 있는 권한 나타냄
    • prefix로 ‘ROLE_’ 이 붙음
    • 인증 이후에 인가를 할 때 사용. 권한은 여러개일 수 있으므로 Collection형태로 제공

인증 과정

  1. username과 password로 UsernamePasswordAuthenticationToken 생성
  2. UsernamePasswordAuthenticationToken은 검증을 위해 AuthenticationManager로 전달
  3. SecurityContext에 Authentication 저장
  4. 이 토큰(UsernamePasswordAuthenticationToken)은 검증을 위해 AuthenticationManager의 인스턴스로 전달됨
  5. AuthenticationManager는 인증에 성공하면 Authentication 인스턴스를 리턴
//login service로직 일부
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dto.getEmail(), dto.getPassword());
      Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
      String authorities = authentication.getAuthorities()
              .stream()
              .map(GrantedAuthority::getAuthority)
              .collect(Collectors.joining(","));

여기서 authenticationManager는 실제 인증 로직이 담긴 AuthenticationProvider를 관리하며, authenticate()메소드를 통해 아직 인증되지 않은 Authentication객체로부터 인증완료된 Authentication을 반환한다.



OncePerRequestFilter vs GenericFilterBean

인증/인가를 구현하기 위해 여러 레포를 참고하였는데, 대체로 filer클래스가OncePerRequestFilter를 상속받는 경우와 GenericFilterBean을 상속받는 경우로 나뉘었다.
둘 다 기본적인 인증 필터 기능에는 문제가 없지만 어떤 차이가 있는지 찾아봤다.

스프링에서, 디스패처 서블릿이 서블릿 컨테이너 앞에서 모든 요청을 컨트롤러에 전달한다.
서블릿은 요청마다 서블릿을 생성하여 메모리에 저장한 뒤 같은 클라이언트의 요청이 들어올 경우 생성해둔 서블릿 객체를 재활용한다.
그런데 만약 서블릿이 다른 서블릿으로 dispatch하게 되면, 다른 서블릿 앞단에서 filter chain을 한번 더 거치게 된다.
이 차이때문에 OncePerRequestFilter를 사용한다.

쉽게 말해, 내부적으로 프로젝트의 다른 API에 요청할 때마다 모든 API가 동일한 보안 필터를 갖기 때문에 동일한 인증이 다시 발생하게 된다.
이를 막기 위해서 OncePerRequest를 상속받아 AuthenticationFilter를 구현하는 것이 더 나은 선택!

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public int tokenPrefixLength = TokenProvider.TOKEN_PREFIX.length();

    private final TokenProvider tokenProvider;

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (bearerToken != null && bearerToken.startsWith(TokenProvider.TOKEN_PREFIX)) {
            return bearerToken.substring(tokenPrefixLength);
        }
        return null;
    }

    // 인증/인가 filter처리 전 토큰을 통해 인증 정보 저장
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = resolveToken(request);

        if (StringUtils.hasText(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}



OnePerRequestFilter도 GenericFilterBean을 상속받은 클래스인데, 어떤 부분이 해당 차이를 결정짓는지 코드로 확인해보자.

	@Override
	public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		if (!((request instanceof HttpServletRequest httpRequest) && (response instanceof HttpServletResponse httpResponse))) {
			throw new ServletException("OncePerRequestFilter only supports HTTP requests");
		}

		String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
		boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;

		if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
			// Proceed without invoking this filter...
			filterChain.doFilter(request, response);
		}
		else if (hasAlreadyFilteredAttribute) {
			if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
				doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
				return;
			}

			// Proceed without invoking this filter...
			filterChain.doFilter(request, response);
		}
		else {
			// Do invoke this filter...
			request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
			try {
				doFilterInternal(httpRequest, httpResponse, filterChain);
			}
			finally {
				// Remove the "already filtered" request attribute for this request.
				request.removeAttribute(alreadyFilteredAttributeName);
			}
		}
	}

alreadyFilteredAttributeName 속성을 통해 요청이 이미 필터링되었는지 확인하고, 이미 필터링되었으면 다시 실행하지 않음으로서 요청 당 한 번만 실행을 보장한다.



Spring Security 설정 후 Swagger가 막힌다?

                .requestMatchers(
                      "/api/v1/auth/**",
                      "/swagger-ui/**",
                      "/swagger-resources/**",
                      "/v3/api-docs/**"
              ).permitAll()

swagger관련 엔드포인트를 permitAll해줌으로서 인증 필터에서 막히지 않도록 설정해주기

1개의 댓글

comment-user-thumbnail
2024년 7월 28일

오타가 많아요ㅠ

답글 달기