프로젝트

[Aper] 채팅 서버 JWT 토큰 인증 + 메인 서버 DB 연결

미뿌감 2024. 10. 9. 15:48
728x90

1. 개요

프로젝트에서 채팅의 기능을 확장하면서, JWT 토큰 인증. 인가를 통한 이용이 필요로 하게 되었다.

또한, 메인 서버의 Mysql DB 연결을 통해서, 채팅 내역과 기록들 또한 넘겨 주어야 할 필요성이 있었다. 

 

특히, 새로 추가된 기능 중 : "채팅방 목록 반환시, 최근에 보낸 메시지와 읽음 여부를 반환" 해주어야 했다.

따라서 이를 해결할 방법으로 

  1. MongoDB 동시에 접근 - 동시성 제어 정합성 필요
  2. MongoDB Replica Set 및 Sharding 사용
  3. 메인 서버에서 채팅 서버로 API 요청
  4. 이벤트 기반 메시징 시스템 (Kafka, RabbitMQ)
  5. 메인 서버의 MySQL 연결 후 사용. - 동시성 제어 정합성 필요 

를 생각해 보았다.

 

우선 메인 서버에서 채팅 서버로 API를 요청하는 것은, 메인 서버가 client이자, server의 역할을 하게 되는 것이라고 생각하였다.

메인 서버에게 크게 부하가 가해질 것이라고 판단. 3번은 deprecate 하였다.

 

반면, 5. 메인 서버의 MySQL 연결 후 사용을 하게 되면, 토큰을 통한 인증. 인가 또한 가능해 지기 때문에 5번을 사용하는 것은 두 마리 토끼를 모두 잡을 수 있는 방법이었다.

 

이에 채팅 서버에 security와 JWT 토큰 분석을 적용시켜 보았다.
application.yml에 아래 코드를 추가해서 필터 적용 과정을 로그에 찍히도록 하였다.

logging:
  level:
    org.springframework.security: DEBUG
    org.springframework.web: DEBUG

 

인증은 필터의 순서대로 작동을 하며, filterChain.doFilter를 통해서 다음 필터로 넘어갈 수 있도록 한다.

filterChain.doFilter(request, response);

 

  1. DisableEncodeUrlFilter
  2. WebAsyncManagerIntegrationFilter
  3. SecuritryContextHolderFilter
  4. HeaderWriterFilter
  5. CorsFilter
  6. LogoutFilter
  7. JwtAuthorizationFilter
  8. RequestCacheAwareFilter
  9. SecurityContextHolderAwareRequestFilter
  10. AnonymousAuthenticationFilter
  11. SessionManagementFilter
  12. ExceptionTranslationFilter

1. DisableEncodeUrlFilter

public class DisableEncodeUrlFilter extends OncePerRequestFilter {
    public DisableEncodeUrlFilter() {
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response));
    }

    private static final class DisableEncodeUrlResponseWrapper extends HttpServletResponseWrapper {
        private DisableEncodeUrlResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        public String encodeRedirectURL(String url) {
            return url;
        }

        public String encodeURL(String url) {
            return url;
        }
    }
}

 

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

 

코드 각각에 대해서 뜯어보자.

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response));
    }

 

filterChain.doFilter를 통해서 다음 필터로 request와 response를 넘겨준다.

이때, response를 DisableEncodeUrlResponseWrapper라는 래퍼 객체로 감싸준다.

이를 하는 이유는, DisableEncodeUrlResponseWrapper와 encodeRedirectURL, encodeURL이라는 메서드를 재정의 하기 위함이다.

encodeURL("http://example.com/some path") ->  http://example.com/some%20path

오른쪽으로 바꾸는 것에 대한 것을 재정의하기 위함이고, 

재정의를 통해 원본 그대로를 반환해준다.

 

요약 : DisableEncodeUrlFilter의 역할 URI를 만지는 기능. 현재 프로젝트에서는 Encode를 막고, 원본 url 그대로를 반환함.

 

2. WebAsyncManagerIntegrationFilter

public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
    private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
    private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

    public WebAsyncManagerIntegrationFilter() {
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor)asyncManager.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
        if (securityProcessingInterceptor == null) {
            SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
            interceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
            asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, interceptor);
        }

        filterChain.doFilter(request, response);
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }
}

WebAsyncManagerIntegrationFilter는 위의 필터 DisableEncodeUrlFilter와 동일하게, OncePerRequestFilter를 extend한다.

비동기 요청에서 SecurityContext를 관리하는 데 사용함.

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

 

 

SecurityContext를 관리하기 위해 우선 SecurityContextHolder에서 strategy를 가져와 준다.

 - Security Context란, 애플리케이션의 보안 상태를 저장하는 데 사용되는 컨텍스트 객체'로 인증된 사용자에 대한 정보를 포함한다.

이는 Authentication 객체로 관리가 되며, 사용자의 아이디, 비밀번호, 권한, 인증 방식 등을 포함한다.

 

또한 Callable Interceptor는 Callable 객체 실행 전후로 특정 작업을 할 수 있도록 지원하는 인터셉터이다.

WebAsyncManager는 비동기 처리를 담당하는 녀석이다.

또한, SecurityContextCallableProcessingInterceptor가 있다면, 찾아주고, 없다면 생성해 준다.

SecurityContextCallableProcessingInterceptor는 비동기 요청을 처리할 때, SecurityContext를 안전하게 복원하기 위한 역할이다.

 

3. SecurityContextHolderFilter

public class SecurityContextHolderFilter extends GenericFilterBean {
    private static final String FILTER_APPLIED = SecurityContextHolderFilter.class.getName() + ".APPLIED";
    private final SecurityContextRepository securityContextRepository;
    private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
        Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
        this.securityContextRepository = securityContextRepository;
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
        } else {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);

            try {
                this.securityContextHolderStrategy.setDeferredContext(deferredContext);
                chain.doFilter(request, response);
            } finally {
                this.securityContextHolderStrategy.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }

        }
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }
}

SecurityContextHolderFilter는 Security Context를 적절히 설정하고, 요청이 완료된 후 이를 정리하는 필터의 역할을 한다.

 

이전에 이미 해당 필터를 거쳤다면, 다시 실행되지 않도록 하고, 처음 실행이 된다면 해당 필터가 실행되었음을 기록하여 준다.

또한 finally로는 security context를 정리할 수 있도록 한다.

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

      |_____ SecurityContextHolderFilter 

              |______ DisableEncodeUrlFilter 

                    |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

 

 

4. HeaderWriterFilter

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.header;

import jakarta.servlet.FilterChain;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.springframework.security.web.util.OnCommittedResponseWrapper;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

public class HeaderWriterFilter extends OncePerRequestFilter {
    private final List<HeaderWriter> headerWriters;
    private boolean shouldWriteHeadersEagerly = false;

    public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
        Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
        this.headerWriters = headerWriters;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (this.shouldWriteHeadersEagerly) {
            this.doHeadersBefore(request, response, filterChain);
        } else {
            this.doHeadersAfter(request, response, filterChain);
        }

    }

    private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        this.writeHeaders(request, response);
        filterChain.doFilter(request, response);
    }

    private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request, response);
        HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request, headerWriterResponse);

        try {
            filterChain.doFilter(headerWriterRequest, headerWriterResponse);
        } finally {
            headerWriterResponse.writeHeaders();
        }

    }

    void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        Iterator var3 = this.headerWriters.iterator();

        while(var3.hasNext()) {
            HeaderWriter writer = (HeaderWriter)var3.next();
            writer.writeHeaders(request, response);
        }

    }

    public void setShouldWriteHeadersEagerly(boolean shouldWriteHeadersEagerly) {
        this.shouldWriteHeadersEagerly = shouldWriteHeadersEagerly;
    }

    class HeaderWriterResponse extends OnCommittedResponseWrapper {
        private final HttpServletRequest request;

        HeaderWriterResponse(HttpServletRequest request, HttpServletResponse response) {
            super(response);
            this.request = request;
        }

        protected void onResponseCommitted() {
            this.writeHeaders();
            this.disableOnResponseCommitted();
        }

        protected void writeHeaders() {
            if (!this.isDisableOnResponseCommitted()) {
                HeaderWriterFilter.this.writeHeaders(this.request, this.getHttpResponse());
            }
        }

        private HttpServletResponse getHttpResponse() {
            return (HttpServletResponse)this.getResponse();
        }
    }

    static class HeaderWriterRequest extends HttpServletRequestWrapper {
        private final HeaderWriterResponse response;

        HeaderWriterRequest(HttpServletRequest request, HeaderWriterResponse response) {
            super(request);
            this.response = response;
        }

        public RequestDispatcher getRequestDispatcher(String path) {
            return new HeaderWriterRequestDispatcher(super.getRequestDispatcher(path), this.response);
        }
    }

    static class HeaderWriterRequestDispatcher implements RequestDispatcher {
        private final RequestDispatcher delegate;
        private final HeaderWriterResponse response;

        HeaderWriterRequestDispatcher(RequestDispatcher delegate, HeaderWriterResponse response) {
            this.delegate = delegate;
            this.response = response;
        }

        public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException {
            this.delegate.forward(request, response);
        }

        public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException {
            this.response.onResponseCommitted();
            this.delegate.include(request, response);
        }
    }
}

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

 

 

HeaderWriterFilter는 헤더에 보안 관련 기능을 추가하는 것이다.

HeaderWriterResponse와 HeaderWriterRequest 객체를 생성하여 요청과 응답을 래핑하고 이후 필터 체인의 다음 필터로 요청을 전달한다.

또한 보안 헤더의 추가 시점도 달리 설정할 수 있다. 메서드로 doHeadersBefore와 doHeadersAfter를 보면, 이를 알 수 있다.

 

5. CorsFilter

public class CorsFilter extends OncePerRequestFilter {
    private final CorsConfigurationSource configSource;
    private CorsProcessor processor = new DefaultCorsProcessor();

    public CorsFilter(CorsConfigurationSource configSource) {
        Assert.notNull(configSource, "CorsConfigurationSource must not be null");
        this.configSource = configSource;
    }

    public void setCorsProcessor(CorsProcessor processor) {
        Assert.notNull(processor, "CorsProcessor must not be null");
        this.processor = processor;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
        boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
        if (isValid && !CorsUtils.isPreFlightRequest(request)) {
            filterChain.doFilter(request, response);
        }
    }
}

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

 

 

CORS는 아시다시피, 다른 도메인 간의 요청을 막는데 도움을 준다.

CORS과련 요청을 처리하고 유효한 데이터들을 담아서, filterChain.doFilter로 다음으로 넘겨준다.

 

6. LogoutFilter

다음으로는 로그아웃 필터이다.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.authentication.logout;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

public class LogoutFilter extends GenericFilterBean {
    private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
    private RequestMatcher logoutRequestMatcher;
    private final LogoutHandler handler;
    private final LogoutSuccessHandler logoutSuccessHandler;

    public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.setFilterProcessesUrl("/logout");
    }

    public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
        this.handler = new CompositeLogoutHandler(handlers);
        Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) || UrlUtils.isValidRedirectUrl(logoutSuccessUrl), () -> {
            return logoutSuccessUrl + " isn't a valid redirect URL";
        });
        SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
        if (StringUtils.hasText(logoutSuccessUrl)) {
            urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
        }

        this.logoutSuccessHandler = urlLogoutSuccessHandler;
        this.setFilterProcessesUrl("/logout");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (this.requiresLogout(request, response)) {
            Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Logging out [%s]", auth));
            }

            this.handler.logout(request, response, auth);
            this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
        } else {
            chain.doFilter(request, response);
        }
    }

    protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
        if (this.logoutRequestMatcher.matches(request)) {
            return true;
        } else {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Did not match request to %s", this.logoutRequestMatcher));
            }

            return false;
        }
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }

    public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
        Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
        this.logoutRequestMatcher = logoutRequestMatcher;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
    }
}

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

 

세션을 무효화하거나, 사용자 인증 정보를 삭제하는 등의 로그아웃 기능을 하는 Filter이다.

 

7. JwtAuthorizationFilter

package com.sparta.aper_chat_back.global.security.filter;

import com.sparta.aper_chat_back.global.security.exception.CustomResponseUtil;
import com.sparta.aper_chat_back.global.security.handler.ErrorCode;
import com.sparta.aper_chat_back.global.security.handler.exception.TokenException;
import com.sparta.aper_chat_back.global.security.jwt.TokenProvider;
import com.sparta.aper_chat_back.global.security.user.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Date;

@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(TokenProvider tokenProvider, UserDetailsServiceImpl userDetailsService) {
        this.tokenProvider = tokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {

        String requestURI = request.getRequestURI();

        // 재발급 요청일 경우 필터를 통과시킴
        if ("/reissue".equals(requestURI)) {
            filterChain.doFilter(request, response);
            return;
        }

        String tokenValue = tokenProvider.getJwtFromHeader(request);

        if (StringUtils.hasText(tokenValue)) {
            try{
                Claims claims = tokenProvider.getUserInfoFromAccessToken(tokenValue);
                if (claims.getExpiration().before(new Date())){
                    CustomResponseUtil.fail(response, ErrorCode.EXPIRED_ACCESS_TOKEN);
                    return;
                }
                String username = claims.getSubject();
                if (username != null) {
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (TokenException e) {
                log.error(e.getMessage());
                SecurityContextHolder.clearContext();
                CustomResponseUtil.fail(response, e.getMessage(), e.getStatus());
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

 

JwtAuthorizationFilter를 더 자세히 알아보면 아래와 같다.

 

JwtAuthorizationFilter - 요청을 가로채서 JWT를 확인, 유효한 토큰이면 인증

     |______ TokenProvider 이용- 토큰을 자르고, 필요한 토큰의 부분만 반환 ex) 헤더에서 JWT 가져오기. User의 정보 가져오기.

     |______ UserDetailsServiceImpl 이용- 토큰에서 찾은 username으로 db에서 해당 유저 정보 가져오기

                           |_______ UsernamePasswordAuthenticationToken - Principal, Credentials, Authorities 로 인증 객체 구성

 

8. RequestCacheAwareFilter

public class RequestCacheAwareFilter extends GenericFilterBean {
    private RequestCache requestCache;

    public RequestCacheAwareFilter() {
        this(new HttpSessionRequestCache());
    }

    public RequestCacheAwareFilter(RequestCache requestCache) {
        Assert.notNull(requestCache, "requestCache cannot be null");
        this.requestCache = requestCache;
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest)request, (HttpServletResponse)response);
        chain.doFilter((ServletRequest)(wrappedSavedRequest != null ? wrappedSavedRequest : request), response);
    }
}

 

인증이 완료된 후에, 사용자가 있던 URL로 돌아갈 수 있도록 하는 Filter이다.

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ RequestCacheAwareFilter

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

 

RequestCacheAwareFilter를 아래와 같이 가져와서 사용하여, 로그인 후 요청했던 페이지로 되돌아갈 수 있도록 설정할 수 있다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .requestCache()
                .requestCache(new HttpSessionRequestCache())  // RequestCache 설정
                .and()
            .addFilterAfter(new RequestCacheAwareFilter(), SecurityContextHolderFilter.class);  // 필터 추가
    }
}

.addFilterAfter로 확인할 수 있다.

 

9. SecurityContextHolderAwareRequestFilter

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ SecurityContextHolderAwareRequestFilter

      |_____ RequestCacheAwareFilter

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

다음으로, SecurityContextHolderAwareRequestFilter 에 대해서 알아보자.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.servletapi;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean {
    private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
    private String rolePrefix = "ROLE_";
    private HttpServletRequestFactory requestFactory;
    private AuthenticationEntryPoint authenticationEntryPoint;
    private AuthenticationManager authenticationManager;
    private List<LogoutHandler> logoutHandlers;
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
    private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();

    public SecurityContextHolderAwareRequestFilter() {
    }

    public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
        Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
        this.securityContextRepository = securityContextRepository;
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }

    public void setRolePrefix(String rolePrefix) {
        Assert.notNull(rolePrefix, "Role prefix must not be null");
        this.rolePrefix = rolePrefix;
        this.updateFactory();
    }

    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
        this.logoutHandlers = logoutHandlers;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(this.requestFactory.create((HttpServletRequest)req, (HttpServletResponse)res), res);
    }

    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        this.updateFactory();
    }

    private void updateFactory() {
        String rolePrefix = this.rolePrefix;
        this.requestFactory = this.createServlet3Factory(rolePrefix);
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        Assert.notNull(trustResolver, "trustResolver cannot be null");
        this.trustResolver = trustResolver;
        this.updateFactory();
    }

    private HttpServletRequestFactory createServlet3Factory(String rolePrefix) {
        HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix, this.securityContextRepository);
        factory.setTrustResolver(this.trustResolver);
        factory.setAuthenticationEntryPoint(this.authenticationEntryPoint);
        factory.setAuthenticationManager(this.authenticationManager);
        factory.setLogoutHandlers(this.logoutHandlers);
        factory.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
        return factory;
    }
}

 

이는 Spring Security에서 HTTP 요청을 래핑하여 보안 컨택스트를 인식하고 인증된 사용자 권한을 쉽게 사용할 수 있도록 돕는 filter이다.

 

public void setRolePrefix(String rolePrefix) {
    Assert.notNull(rolePrefix, "Role prefix must not be null");
    this.rolePrefix = rolePrefix;
    this.updateFactory();
}

 

이를 이용해서 ROLE_ADMIN과 같이 사용자가 admin권한을 가지고 있는지도 확인할 수 있다.

 

10. AnonymousAuthenticationFilter

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ AnonymousAuthenticationFilter

      |_____ SecurityContextHolderAwareRequestFilter

      |_____ RequestCacheAwareFilter

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.authentication;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.Assert;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.filter.GenericFilterBean;

public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
    private SecurityContextHolderStrategy securityContextHolderStrategy;
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
    private String key;
    private Object principal;
    private List<GrantedAuthority> authorities;

    public AnonymousAuthenticationFilter(String key) {
        this(key, "anonymousUser", AuthorityUtils.createAuthorityList(new String[]{"ROLE_ANONYMOUS"}));
    }

    public AnonymousAuthenticationFilter(String key, Object principal, List<GrantedAuthority> authorities) {
        this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
        this.authenticationDetailsSource = new WebAuthenticationDetailsSource();
        Assert.hasLength(key, "key cannot be null or empty");
        Assert.notNull(principal, "Anonymous authentication principal must be set");
        Assert.notNull(authorities, "Anonymous authorities must be set");
        this.key = key;
        this.principal = principal;
        this.authorities = authorities;
    }

    public void afterPropertiesSet() {
        Assert.hasLength(this.key, "key must have length");
        Assert.notNull(this.principal, "Anonymous authentication principal must be set");
        Assert.notNull(this.authorities, "Anonymous authorities must be set");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        Supplier<SecurityContext> deferredContext = this.securityContextHolderStrategy.getDeferredContext();
        this.securityContextHolderStrategy.setDeferredContext(this.defaultWithAnonymous((HttpServletRequest)req, deferredContext));
        chain.doFilter(req, res);
    }

    private Supplier<SecurityContext> defaultWithAnonymous(HttpServletRequest request, Supplier<SecurityContext> currentDeferredContext) {
        return SingletonSupplier.of(() -> {
            SecurityContext currentContext = (SecurityContext)currentDeferredContext.get();
            return this.defaultWithAnonymous(request, currentContext);
        });
    }

    private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) {
        Authentication currentAuthentication = currentContext.getAuthentication();
        if (currentAuthentication == null) {
            Authentication anonymous = this.createAuthentication(request);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.of(() -> {
                    return "Set SecurityContextHolder to " + anonymous;
                }));
            } else {
                this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
            }

            SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext();
            anonymousContext.setAuthentication(anonymous);
            return anonymousContext;
        } else {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.of(() -> {
                    return "Did not set SecurityContextHolder since already authenticated " + currentAuthentication;
                }));
            }

            return currentContext;
        }
    }

    protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities);
        token.setDetails(this.authenticationDetailsSource.buildDetails(request));
        return token;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public List<GrantedAuthority> getAuthorities() {
        return this.authorities;
    }
}

 

해당 filter의 역할은 인증되지 않은 사용자에 대해서 제한된 접근을 허용해야할 때, 사용이 되곤 한다.

 

11. SessionManagementFilter

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ SessionManagementFilter

      |_____ AnonymousAuthenticationFilter

      |_____ SecurityContextHolderAwareRequestFilter

      |_____ RequestCacheAwareFilter

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.session;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

public class SessionManagementFilter extends GenericFilterBean {
    static final String FILTER_APPLIED = "__spring_security_session_mgmt_filter_applied";
    private SecurityContextHolderStrategy securityContextHolderStrategy;
    private final SecurityContextRepository securityContextRepository;
    private SessionAuthenticationStrategy sessionAuthenticationStrategy;
    private AuthenticationTrustResolver trustResolver;
    private InvalidSessionStrategy invalidSessionStrategy;
    private AuthenticationFailureHandler failureHandler;

    public SessionManagementFilter(SecurityContextRepository securityContextRepository) {
        this(securityContextRepository, new SessionFixationProtectionStrategy());
    }

    public SessionManagementFilter(SecurityContextRepository securityContextRepository, SessionAuthenticationStrategy sessionStrategy) {
        this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
        this.trustResolver = new AuthenticationTrustResolverImpl();
        this.invalidSessionStrategy = null;
        this.failureHandler = new SimpleUrlAuthenticationFailureHandler();
        Assert.notNull(securityContextRepository, "SecurityContextRepository cannot be null");
        Assert.notNull(sessionStrategy, "SessionAuthenticationStrategy cannot be null");
        this.securityContextRepository = securityContextRepository;
        this.sessionAuthenticationStrategy = sessionStrategy;
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getAttribute("__spring_security_session_mgmt_filter_applied") != null) {
            chain.doFilter(request, response);
        } else {
            request.setAttribute("__spring_security_session_mgmt_filter_applied", Boolean.TRUE);
            if (!this.securityContextRepository.containsContext(request)) {
                Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
                if (this.trustResolver.isAuthenticated(authentication)) {
                    try {
                        this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
                    } catch (SessionAuthenticationException var6) {
                        this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", var6);
                        this.securityContextHolderStrategy.clearContext();
                        this.failureHandler.onAuthenticationFailure(request, response, var6);
                        return;
                    }

                    this.securityContextRepository.saveContext(this.securityContextHolderStrategy.getContext(), request, response);
                } else if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug(LogMessage.format("Request requested invalid session id %s", request.getRequestedSessionId()));
                    }

                    if (this.invalidSessionStrategy != null) {
                        this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
                        return;
                    }
                }
            }

            chain.doFilter(request, response);
        }
    }

    public void setInvalidSessionStrategy(InvalidSessionStrategy invalidSessionStrategy) {
        this.invalidSessionStrategy = invalidSessionStrategy;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        Assert.notNull(trustResolver, "trustResolver cannot be null");
        this.trustResolver = trustResolver;
    }

    public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
        Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
        this.securityContextHolderStrategy = securityContextHolderStrategy;
    }
}

 

이는 세션 관리와 관련된 역할을 수행한다. 세션 고정 공격을 방지한다.

 

12. ExceptionTranslationFilter

해당 필터는 생성된 예외에 대해서 응답 처리를 해주는 코드이다.

 

GenericFilterBean - OncePerRequestFilter를 extend 함.

      |_____ ExceptionTranslationFilter

      |_____ SessionManagementFilter

      |_____ AnonymousAuthenticationFilter

      |_____ SecurityContextHolderAwareRequestFilter

      |_____ RequestCacheAwareFilter

      |_____ LogoutFilter

      |_____OncePerRequestFilter - Spring Framework에서 제공. 각 요청 당 한번만 Filter를 거치도록 도움. 안에 dofilter도 있음.

              |______ DisableEncodeUrlFilter 

                      |______ DisableEncodeUrlResponseWrapper <--extends --> HttpServletResponseWrapper (재정의 위함)

                                      |______ DisableEncodeUrlResponseWrapper, encodeRedirectURL, encodeURL 메서드 재정의

              |______ WebAsyncManagerIntegrationFilter

              |______ HeaderWriterFilter

              |______ CorsFilter

              |______ JwtAuthorizationFilter

 

 

728x90