본문 바로가기

Backend Development/Spring boot

[Spring boot] Spring Security 분석 - FilterSecurityInterceptor (Token 인증 기반)

특정 페이지에 권한없는 계정으로 접근 시 아래와 같이 Exception 처리가 된다.

 

handleAccessDeniedException:185, ExceptionTranslationFilter (org.springframework.security.web.access)
handleSpringSecurityException:173, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:142, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:115, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:126, SessionManagementFilter (org.springframework.security.web.session)
doFilter:81, SessionManagementFilter (org.springframework.security.web.session)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:105, AnonymousAuthenticationFilter (org.springframework.security.web.authentication)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:149, SecurityContextHolderAwareRequestFilter (org.springframework.security.web.servletapi)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:63, RequestCacheAwareFilter (org.springframework.security.web.savedrequest)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:47, TokenAuthenticationFilter (com.sdp.common.security)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:103, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:89, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doHeadersAfter:90, HeaderWriterFilter (org.springframework.security.web.header)
doFilterInternal:75, HeaderWriterFilter (org.springframework.security.web.header)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:110, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:80, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:55, WebAsyncManagerIntegrationFilter (org.springframework.security.web.context.request.async)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:211, FilterChainProxy (org.springframework.security.web)
doFilter:183, FilterChainProxy (org.springframework.security.web)
invokeDelegate:358, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:271, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:834, Thread (java.lang)

 

org.springframework.security.access.AccessDeniedException: Access is denied Call Stack

더보기

stackTrace = {StackTraceElement[68]@11849} 
 0 = {StackTraceElement@11851} "org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73)"
 1 = {StackTraceElement@11852} "org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:238)"
 2 = {StackTraceElement@11853} "org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:208)"
 3 = {StackTraceElement@11854} "org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:113)"
 4 = {StackTraceElement@11855} "org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)"
 5 = {StackTraceElement@11856} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 6 = {StackTraceElement@11857} "org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:121)"
 7 = {StackTraceElement@11858} "org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115)"
 8 = {StackTraceElement@11859} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 9 = {StackTraceElement@11860} "org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126)"
 10 = {StackTraceElement@11861} "org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)"
 11 = {StackTraceElement@11862} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 12 = {StackTraceElement@11863} "org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:105)"
 13 = {StackTraceElement@11864} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 14 = {StackTraceElement@11865} "org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149)"
 15 = {StackTraceElement@11866} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 16 = {StackTraceElement@11867} "org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)"
 17 = {StackTraceElement@11868} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 18 = {StackTraceElement@11869} "com.sdp.common.security.TokenAuthenticationFilter.doFilterInternal(TokenAuthenticationFilter.java:47)"
 19 = {StackTraceElement@11870} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 20 = {StackTraceElement@11871} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 21 = {StackTraceElement@11872} "org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)"
 22 = {StackTraceElement@11873} "org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)"
 23 = {StackTraceElement@11874} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 24 = {StackTraceElement@11875} "org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)"
 25 = {StackTraceElement@11876} "org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)"
 26 = {StackTraceElement@11877} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 27 = {StackTraceElement@11878} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 28 = {StackTraceElement@11879} "org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110)"
 29 = {StackTraceElement@11880} "org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80)"
 30 = {StackTraceElement@11881} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 31 = {StackTraceElement@11882} "org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)"
 32 = {StackTraceElement@11883} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 33 = {StackTraceElement@11884} "org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336)"
 34 = {StackTraceElement@11885} "org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211)"
 35 = {StackTraceElement@11886} "org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183)"
 36 = {StackTraceElement@11887} "org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)"
 37 = {StackTraceElement@11888} "org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)"
 38 = {StackTraceElement@11889} "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)"
 39 = {StackTraceElement@11890} "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)"
 40 = {StackTraceElement@11891} "org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)"
 41 = {StackTraceElement@11892} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 42 = {StackTraceElement@11893} "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)"
 43 = {StackTraceElement@11894} "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)"
 44 = {StackTraceElement@11895} "org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)"
 45 = {StackTraceElement@11896} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 46 = {StackTraceElement@11897} "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)"
 47 = {StackTraceElement@11898} "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)"
 48 = {StackTraceElement@11899} "org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)"
 49 = {StackTraceElement@11900} "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)"
 50 = {StackTraceElement@11901} "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)"
 51 = {StackTraceElement@11902} "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)"
 52 = {StackTraceElement@11903} "org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)"
 53 = {StackTraceElement@11904} "org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)"
 54 = {StackTraceElement@11905} "org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)"
 55 = {StackTraceElement@11906} "org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)"
 56 = {StackTraceElement@11907} "org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)"
 57 = {StackTraceElement@11908} "org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)"
 58 = {StackTraceElement@11909} "org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)"
 59 = {StackTraceElement@11910} "org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)"
 60 = {StackTraceElement@11911} "org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)"
 61 = {StackTraceElement@11912} "org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)"
 62 = {StackTraceElement@11913} "org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722)"
 63 = {StackTraceElement@11914} "org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)"
 64 = {StackTraceElement@11915} "org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)"
 65 = {StackTraceElement@11916} "org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)"
 66 = {StackTraceElement@11917} "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)"
 67 = {StackTraceElement@11918} "java.base/java.lang.Thread.run(Thread.java:834)"
depth = 68
suppressedExceptions = {Collections$EmptyList@8861}  size = 0

 

Spring Security는 인가를 할때 AccessDecisionManager를 사용한다.

 

FilterSecurityInterceptor

 

  • FilterChainProxy가 호출하는 Filter중 하나이다.
  • 대부분의 경우에 가장 마지막에 사용된다.
  • 어떤 리소스에 접근하기 전 마지막에 AccessDecisionManager를 사용하여 인가처리를 하는 필터이다.
/**
 * Performs security handling of HTTP resources via a filter implementation.
 * <p>
 * The <code>SecurityMetadataSource</code> required by this security interceptor is of
 * type {@link FilterInvocationSecurityMetadataSource}.
 * <p>
 * Refer to {@link AbstractSecurityInterceptor} for details on the workflow.
 * </p>
 *
 * @author Ben Alex
 * @author Rob Winch
 */
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

	private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

	private FilterInvocationSecurityMetadataSource securityMetadataSource;

	private boolean observeOncePerRequest = true;

	/**
	 * Not used (we rely on IoC container lifecycle services instead)
	 * @param arg0 ignored
	 *
	 */
	@Override
	public void init(FilterConfig arg0) {
	}

	/**
	 * Not used (we rely on IoC container lifecycle services instead)
	 */
	@Override
	public void destroy() {
	}

	/**
	 * Method that is actually called by the filter chain. Simply delegates to the
	 * {@link #invoke(FilterInvocation)} method.
	 * @param request the servlet request
	 * @param response the servlet response
	 * @param chain the filter chain
	 * @throws IOException if the filter chain fails
	 * @throws ServletException if the filter chain fails
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		invoke(new FilterInvocation(request, response, chain));
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	@Override
	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}

	@Override
	public Class<?> getSecureObjectClass() {
		return FilterInvocation.class;
	}

	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// first time this request being called, so perform security checking
		if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
			filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}

	private boolean isApplied(FilterInvocation filterInvocation) {
		return (filterInvocation.getRequest() != null)
				&& (filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null);
	}

	/**
	 * Indicates whether once-per-request handling will be observed. By default this is
	 * <code>true</code>, meaning the <code>FilterSecurityInterceptor</code> will only
	 * execute once-per-request. Sometimes users may wish it to execute more than once per
	 * request, such as when JSP forwards are being used and filter security is desired on
	 * each included fragment of the HTTP request.
	 * @return <code>true</code> (the default) if once-per-request is honoured, otherwise
	 * <code>false</code> if <code>FilterSecurityInterceptor</code> will enforce
	 * authorizations for each and every fragment of the HTTP request.
	 */
	public boolean isObserveOncePerRequest() {
		return this.observeOncePerRequest;
	}

	public void setObserveOncePerRequest(boolean observeOncePerRequest) {
		this.observeOncePerRequest = observeOncePerRequest;
	}

}

 

FilterSecurityInterceptor의 프로세스

 

doFilter

  • FilterSecurityInterceptor도 Filter이기때문에 doFilter 메서드가 호출된다.
  • doFilter 메서드에서는 FilterSecurityInterceptor의 invoke메서드를 호출하게된다.
	/**
	 * Method that is actually called by the filter chain. Simply delegates to the
	 * {@link #invoke(FilterInvocation)} method.
	 * @param request the servlet request
	 * @param response the servlet response
	 * @param chain the filter chain
	 * @throws IOException if the filter chain fails
	 * @throws ServletException if the filter chain fails
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		invoke(new FilterInvocation(request, response, chain));
	}

invoke

  • invoke 메서드에서는 부모클래스인 AbstractSecurityInterceptor의 method 호출을 통해 인가 처리를 진행한다.
	public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// first time this request being called, so perform security checking
		if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
			filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}

 

 

AbstractSecurityInterceptor

  • FilterSecurityInterceptor 의 부모클래스이고 accessDecisionManager를 호출하여 인가를 한다.
  • 어떤 리소스에 접근하더도 이 Filter가 동작한다.
  • 인가에 실패한다면 AccessDeniedException 이벤트를 발생시키고
  • 해당 예외를 처리하는 ExceptionHandling Filter가 존재하고 해당 필터가 처리를 하게되고, 로그인페이지로 리다이렉트한다.
/**
 * Abstract class that implements security interception for secure objects.
 * <p>
 * The <code>AbstractSecurityInterceptor</code> will ensure the proper startup
 * configuration of the security interceptor. It will also implement the proper handling
 * of secure object invocations, namely:
 * <ol>
 * <li>Obtain the {@link Authentication} object from the
 * {@link SecurityContextHolder}.</li>
 * <li>Determine if the request relates to a secured or public invocation by looking up
 * the secure object request against the {@link SecurityMetadataSource}.</li>
 * <li>For an invocation that is secured (there is a list of <code>ConfigAttribute</code>s
 * for the secure object invocation):
 * <ol type="a">
 * <li>If either the
 * {@link org.springframework.security.core.Authentication#isAuthenticated()} returns
 * <code>false</code>, or the {@link #alwaysReauthenticate} is <code>true</code>,
 * authenticate the request against the configured {@link AuthenticationManager}. When
 * authenticated, replace the <code>Authentication</code> object on the
 * <code>SecurityContextHolder</code> with the returned value.</li>
 * <li>Authorize the request against the configured {@link AccessDecisionManager}.</li>
 * <li>Perform any run-as replacement via the configured {@link RunAsManager}.</li>
 * <li>Pass control back to the concrete subclass, which will actually proceed with
 * executing the object. A {@link InterceptorStatusToken} is returned so that after the
 * subclass has finished proceeding with execution of the object, its finally clause can
 * ensure the <code>AbstractSecurityInterceptor</code> is re-called and tidies up
 * correctly using {@link #finallyInvocation(InterceptorStatusToken)}.</li>
 * <li>The concrete subclass will re-call the <code>AbstractSecurityInterceptor</code> via
 * the {@link #afterInvocation(InterceptorStatusToken, Object)} method.</li>
 * <li>If the <code>RunAsManager</code> replaced the <code>Authentication</code> object,
 * return the <code>SecurityContextHolder</code> to the object that existed after the call
 * to <code>AuthenticationManager</code>.</li>
 * <li>If an <code>AfterInvocationManager</code> is defined, invoke the invocation manager
 * and allow it to replace the object due to be returned to the caller.</li>
 * </ol>
 * </li>
 * <li>For an invocation that is public (there are no <code>ConfigAttribute</code>s for
 * the secure object invocation):
 * <ol type="a">
 * <li>As described above, the concrete subclass will be returned an
 * <code>InterceptorStatusToken</code> which is subsequently re-presented to the
 * <code>AbstractSecurityInterceptor</code> after the secure object has been executed. The
 * <code>AbstractSecurityInterceptor</code> will take no further action when its
 * {@link #afterInvocation(InterceptorStatusToken, Object)} is called.</li>
 * </ol>
 * </li>
 * <li>Control again returns to the concrete subclass, along with the <code>Object</code>
 * that should be returned to the caller. The subclass will then return that result or
 * exception to the original caller.</li>
 * </ol>
 *
 * @author Ben Alex
 * @author Rob Winch
 */
public abstract class AbstractSecurityInterceptor
		implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {

	protected final Log logger = LogFactory.getLog(getClass());

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	private ApplicationEventPublisher eventPublisher;

	private AccessDecisionManager accessDecisionManager;

	private AfterInvocationManager afterInvocationManager;

	private AuthenticationManager authenticationManager = new NoOpAuthenticationManager();

	private RunAsManager runAsManager = new NullRunAsManager();

	private boolean alwaysReauthenticate = false;

	private boolean rejectPublicInvocations = false;

	private boolean validateConfigAttributes = true;

	private boolean publishAuthorizationSuccess = false;

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(getSecureObjectClass(), "Subclass must provide a non-null response to getSecureObjectClass()");
		Assert.notNull(this.messages, "A message source must be set");
		Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
		Assert.notNull(this.accessDecisionManager, "An AccessDecisionManager is required");
		Assert.notNull(this.runAsManager, "A RunAsManager is required");

 

 

beforeInvocation

  • 요청이 들어오면 FilterSecurityInterceptor의 invoke 메서드를 호출하게되고, invoke 메서드에서 AbstractSecurityInterceptor의 beforeInvocation 메서드를 호출하여 인가를 진행하게 된다.
protected InterceptorStatusToken beforeInvocation(Object object) {
    Assert.notNull(object, "Object was null");
    if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
        throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
                + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                + getSecureObjectClass());
    }
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
    if (CollectionUtils.isEmpty(attributes)) {
        Assert.isTrue(!this.rejectPublicInvocations,
                () -> "Secure object invocation " + object
                        + " was denied as public invocations are not allowed via this interceptor. "
                        + "This indicates a configuration error because the "
                        + "rejectPublicInvocations property is set to 'true'");
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Authorized public object %s", object));
        }
        publishEvent(new PublicInvocationEvent(object));
        return null; // no further work post-invocation
    }
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
                "An Authentication object was not found in the SecurityContext"), object, attributes);
    }
    Authentication authenticated = authenticateIfRequired();
    if (this.logger.isTraceEnabled()) {
        this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
    }
    // Attempt authorization
    attemptAuthorization(object, attributes, authenticated);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
    }
    if (this.publishAuthorizationSuccess) {
        publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    }

    // Attempt to run as a different user
    Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
    if (runAs != null) {
        SecurityContext origCtx = SecurityContextHolder.getContext();
        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
        SecurityContextHolder.getContext().setAuthentication(runAs);

        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
        }
        // need to revert to token.Authenticated post-invocation
        return new InterceptorStatusToken(origCtx, true, attributes, object);
    }
    this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
    // no further work post-invocation
    return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

}

 

beforeInvocation 메서드 내에서 인가처리를 하는 부분이다.
AccessDecisionManager를 사용하여 인가처리를 진행하고, 만약 인가처리를 하는 과정에서 예외가 발생하게 되면 해당 예외에 대한 이벤트를 발생시키고 SpringSecurity의 ExceptionHandling 처리를 담당하는 Filter에게 이를 위임한다.

 

private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
        Authentication authenticated) {
    try {
        this.accessDecisionManager.decide(authenticated, object, attributes);
    }
    catch (AccessDeniedException ex) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
                    attributes, this.accessDecisionManager));
        }
        else if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
        }
        publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
        throw ex;
    }
}

 

정리

  • Spring Security에서 인가처리를 담당하는 Filter 는 FilterSecurityInterceptor이다.
  • 이는 다른 필터들과 마찬가지로 FilterChainProxy에 의해 호출되며, 대부분의 경우에 가장 마지막에 호출된다.
  • FilterSecurityInterceptor는 부모 클래스인 AbstractSecurityInterceptor의 메서드를 호출하여 인가처리를 하게되는데
  • 해당 메서드 내부에서는 AccessDecisionManager를 사용하여 인가처리를 진행하고, 처리 과정중 발생한 예외는 SpringSecurity의 ExceptionHandling 처리를 담당하는 Filter에게 위임한다.
  • Spring Security의 인가처리 구조
    • FilterChainProxy (호출)-> FilterSecurityInterceptor (AbstractSecurityInterceptor) (호출)-> AccessDecisionManager (호출)-> AccessDecisionVoter (호출)-> ExpressionHandler (처리)

 

Spring Security 인증과 인가 정리

인증

  • Spring Security에서의 인증은 AuthenticaitonManager가 담당한다.
  • AuthenticaitonManager는 우리가 등록한 UserDetailsService를 사용해 인증을 진행한다.
  • 인증이 완료되면 Authentication객체를 반환하게 되는데 Authentication 객체가 가지고 있는 주요 속성들은 다음과 같다.
    • Principal: UserDetailsService에서 반환한 객체
    • GrantedAuthorities: 사용자의 권한
  • 인증을 하는 과정에서 비활성, 잘못된 패스워드, 잠긴 계정 등의 예외를 던질 수 있다.
  • 이러한 Authentication 객체는 SecurityContext(ThreadLocal)에 보관되고, SecurityContext는 SecurityContextHolder를 통해 접근이 가능하다.
  • UsernamePasswordAuthenticationFilter(인증을 담당), SecurityContextPersisenceFilter(Http Session Cache[기본전략]전략으로 Authenticaiton을 캐싱함)가 SecurityContextHolder에 Authentication 객체를 저장하게 된다.

인가

  • Spring Security의 인가는 AccessDecisionManager가 담당한다.
  • AccessDecisionManager는 Voter들을 사용하며 인가에 사용되는 여러 전략들이 존재한다.
  • AccessDecisionVoter는 ExpressionHandler를 사용하여 인가처리를 하게 된다.
  • 이러한 AccessDecisionManager는 FilterSecurityInterceptor(AbstractSecurityInterceptor)가 호출하고, FilterSecurityInterceptor는 FilterChainProxy에 의해 호출된다.
  • Security의 이러한 필터들은 FilterChainProxy가 호출을 담당하며, FilterChainProxy는 DelegatingFilterProxy에게 요청을 위임 받는 구조이다.