spring security 4 filter 顺序及作用
Spring Security 有两个作用:认证和授权
一、Srping security 4 filter 别名及顺序
spring security 4 标准filter别名和顺序,因为经常要用就保存到自己博客吧 点击访问官网链接
Table 6.1. Standard Filter Aliases and Ordering
Alias | Filter Class | Namespace Element or Attribute |
---|---|---|
CHANNEL_FILTER |
|
|
SECURITY_CONTEXT_FILTER |
|
|
CONCURRENT_SESSION_FILTER |
|
|
HEADERS_FILTER |
|
|
CSRF_FILTER |
|
|
LOGOUT_FILTER |
|
|
X509_FILTER |
|
|
PRE_AUTH_FILTER |
|
N/A |
CAS_FILTER |
|
N/A |
FORM_LOGIN_FILTER |
|
|
BASIC_AUTH_FILTER |
|
|
SERVLET_API_SUPPORT_FILTER |
|
|
JAAS_API_SUPPORT_FILTER |
|
|
REMEMBER_ME_FILTER |
|
|
ANONYMOUS_FILTER |
|
|
SESSION_MANAGEMENT_FILTER |
|
|
EXCEPTION_TRANSLATION_FILTER |
|
|
FILTER_SECURITY_INTERCEPTOR |
|
|
SWITCH_USER_FILTER |
|
N/A |
二、Spring security filter作用
2.1 默认filter链
在程序启动时会打印出如下日志,该日志打印出了默认的filter链和顺序,其中SecurityContextPersistenceFilter为第一个filter,FilterSecurityInterceptor为最后一个filter。
2018-02-11 15:24:17,204 INFO DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.SecurityContextPersistenceFilter@3cf3957d, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7ff34bd, org.springframework.security.web.header.HeaderWriterFilter@4dad11a2, org.springframework.security.web.authentication.logout.LogoutFilter@5be6ee89, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@5426eed3, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5da2a66c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@23169e35, org.springframework.security.web.session.SessionManagementFilter@5b1627ea, org.springframework.security.web.access.ExceptionTranslationFilter@70b913f5, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2dfe7327]
2.2 默认filter链作用
默认有10条过滤链,下面逐个看下去。
2.2.1 /index.html at position 1 of 10 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
SecurityContextPersistenceFilter 两个主要职责:
a.请求到来时,通过HttpSessionSecurityContextRepository接口从Session中读取SecurityContext,如果读取结果为null,则创建之。
1 public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { 2 HttpServletRequest request = requestResponseHolder.getRequest(); 3 HttpServletResponse response = requestResponseHolder.getResponse(); 4 HttpSession httpSession = request.getSession(false); 5 // 从session中获取SecurityContext 6 SecurityContext context = readSecurityContextFromSession(httpSession); 7 8 if (context == null) { 9 if (logger.isDebugEnabled()) { 10 logger.debug("No SecurityContext was available from the HttpSession: " 11 + httpSession + ". " + "A new one will be created."); 12 } 13 // 未读取到SecurityContext则新建一个SecurityContext 14 context = generateNewContext(); 15 16 } 17 18 SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper( 19 response, request, httpSession != null, context); 20 requestResponseHolder.setResponse(wrappedResponse); 21 22 if (isServlet3) { 23 requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper( 24 request, wrappedResponse)); 25 } 26 27 return context; 28 }
获得SecurityContext之后,会将其存入SecurityContextHolder,其中SecurityContextHolder默认是ThreadLocalSecurityContextHolderStrategy实例
1 private static void initialize() { 2 if ((strategyName == null) || "".equals(strategyName)) { 3 // Set default 4 strategyName = MODE_THREADLOCAL; 5 } 6 7 if (strategyName.equals(MODE_THREADLOCAL)) { 8 strategy = new ThreadLocalSecurityContextHolderStrategy(); 9 } 10 // 以下内容省略 11 }
ThreadLocalSecurityContextHolderStrategy中的ContextHolder定义如下,注意这是一个ThreadLocal变量,线程局部变量。
1 private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();
b.请求结束时清空SecurityContextHolder,并将SecurityContext保存到Session中。
1 finally { 2 SecurityContext contextAfterChainExecution = SecurityContextHolder 3 .getContext(); 4 // Crucial removal of SecurityContextHolder contents - do this before anything 5 // else. 6 SecurityContextHolder.clearContext(); 7 repo.saveContext(contextAfterChainExecution, holder.getRequest(), 8 holder.getResponse()); 9 request.removeAttribute(FILTER_APPLIED); 10 11 if (debug) { 12 logger.debug("SecurityContextHolder now cleared, as request processing completed"); 13 } 14 }
2.2.2 /index.html at position 2 of 10 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
提供了对securityContext和WebAsyncManager的集成,其会把SecurityContext设置到异步线程中,使其也能获取到用户上下文认证信息。
1 @Override 2 protected void doFilterInternal(HttpServletRequest request, 3 HttpServletResponse response, FilterChain filterChain) 4 throws ServletException, IOException { 5 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 6 7 SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager 8 .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); 9 if (securityProcessingInterceptor == null) { 10 asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, 11 new SecurityContextCallableProcessingInterceptor()); 12 } 13 14 filterChain.doFilter(request, response); 15 }
2.2.3 /index.html at position 3 of 10 in additional filter chain; firing Filter: 'HeaderWriterFilter'
用来给http response添加一些Header,比如X-Frame-Options、X-XSS-Protection*、X-Content-Type-Options。
1 protected void doFilterInternal(HttpServletRequest request, 2 HttpServletResponse response, FilterChain filterChain) 3 throws ServletException, IOException { 4 5 HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request, 6 response, this.headerWriters); 7 try { 8 filterChain.doFilter(request, headerWriterResponse); 9 } 10 finally { 11 // 向response header中添加header 12 headerWriterResponse.writeHeaders(); 13 } 14 }
2.2.4 /index.html at position 4 of 10 in additional filter chain; firing Filter: 'LogoutFilter'
处理退出登录的Filter,如果请求的url为/logout则会执行退出登录操作。
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 HttpServletRequest request = (HttpServletRequest) req; 4 HttpServletResponse response = (HttpServletResponse) res; 5 // 判断是否需要logout,判断request url是否匹配/logout 6 if (requiresLogout(request, response)) { 7 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 8 9 if (logger.isDebugEnabled()) { 10 logger.debug("Logging out user '" + auth 11 + "' and transferring to logout destination"); 12 } 13 // 执行一系列的退出登录操作 14 for (LogoutHandler handler : handlers) { 15 handler.logout(request, response, auth); 16 } 17 // 退出成功,执行logoutSuccessHandler进行重定向等操作 18 logoutSuccessHandler.onLogoutSuccess(request, response, auth); 19 20 return; 21 } 22 23 chain.doFilter(request, response); 24 }
2.2.5 /index.html at position 5 of 10 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
表单认证是最常用的一个认证方式,一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录,而这背后的UsernamePasswordAuthenticationFilter,在整个Spring Security的认证体系中则扮演着至关重要的角色。
UsernamePasswordAuthenticationFilter是继承自AbstractAuthenticationProcessingFilter。
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 4 HttpServletRequest request = (HttpServletRequest) req; 5 HttpServletResponse response = (HttpServletResponse) res; 6 // 判断是否需要执行登录认证,判断request url 是否能匹配/login 7 if (!requiresAuthentication(request, response)) { 8 chain.doFilter(request, response); 9 10 return; 11 } 12 13 if (logger.isDebugEnabled()) { 14 logger.debug("Request is to process authentication"); 15 } 16 17 Authentication authResult; 18 19 try { 20 // UsernamePasswordAuthenticationFilter 实现该方法 21 authResult = attemptAuthentication(request, response); 22 if (authResult == null) { 23 // 子类未完成认证,立即返回 24 return; 25 } 26 sessionStrategy.onAuthentication(authResult, request, response); 27 } 28 // 在认证过程中抛出异常 29 catch (InternalAuthenticationServiceException failed) { 30 logger.error( 31 "An internal error occurred while trying to authenticate the user.", 32 failed); 33 unsuccessfulAuthentication(request, response, failed); 34 35 return; 36 } 37 catch (AuthenticationException failed) { 38 // Authentication failed 39 unsuccessfulAuthentication(request, response, failed); 40 41 return; 42 } 43 44 // Authentication success 45 if (continueChainBeforeSuccessfulAuthentication) { 46 chain.doFilter(request, response); 47 } 48 49 successfulAuthentication(request, response, chain, authResult); 50 }
在UsernamePasswordAuthenticationFilter中实现了类attemptAuthentication,不过该类只实现了一个非常简化的版本,如果真的需要通过表单登录,是需要自己继承UsernamePasswordAuthenticationFilter并重载attemptAuthentication方法的。
在AbstractAuthenticationProcessingFilter的doFilter方法中一开始是判断是否有必要进入到认证filter,这个过程其实是判断request url是否匹配/login,当然也可以通过filterProcessesUrl属性去配置匹配所使用的pattern。
2.2.6 /index.html at position 6 of 10 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
将request存到session中,用于缓存request请求,可以用于恢复被登录而打断的请求
1 public void doFilter(ServletRequest request, ServletResponse response, 2 FilterChain chain) throws IOException, ServletException { 3 // 从session中获取与当前request匹配的缓存request,并将缓存request从session删除 4 HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest( 5 (HttpServletRequest) request, (HttpServletResponse) response); 6 // 如果requestCache中缓存了request,则使用缓存的request 7 chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest, 8 response); 9 }
此处从session中取出request,存储request是在ExceptionTranslationFilter中。具体可以参考探究 Spring Security 缓存请求
2.2.7 /index.html at position 7 of 10 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
此过滤器对ServletRequest进行了一次包装,使得request具有更加丰富的API
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 chain.doFilter(this.requestFactory.create((HttpServletRequest) req, 4 (HttpServletResponse) res), res); 5 }
2.2.8 /index.html at position 8 of 10 in additional filter chain; firing Filter: 'SessionManagementFilter'
和session相关的过滤器,内部维护了一个SessionAuthenticationStrategy,两者组合使用,常用来防止session-fixation protection attack
,以及限制同一用户开启多个会话的数量
与登录认证拦截时作用一样,持久化用户登录信息,可以保存到session中,也可以保存到cookie或者redis中。
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 HttpServletRequest request = (HttpServletRequest) req; 4 HttpServletResponse response = (HttpServletResponse) res; 5 6 if (request.getAttribute(FILTER_APPLIED) != null) { 7 chain.doFilter(request, response); 8 return; 9 } 10 11 request.setAttribute(FILTER_APPLIED, Boolean.TRUE); 12 13 if (!securityContextRepository.containsContext(request)) { 14 Authentication authentication = SecurityContextHolder.getContext() 15 .getAuthentication(); 16 17 if (authentication != null && !trustResolver.isAnonymous(authentication)) { 18 // The user has been authenticated during the current request, so call the 19 // session strategy 20 try { 21 sessionAuthenticationStrategy.onAuthentication(authentication, 22 request, response); 23 } 24 catch (SessionAuthenticationException e) { 25 // The session strategy can reject the authentication 26 logger.debug( 27 "SessionAuthenticationStrategy rejected the authentication object", 28 e); 29 SecurityContextHolder.clearContext(); 30 failureHandler.onAuthenticationFailure(request, response, e); 31 32 return; 33 } 34 // Eagerly save the security context to make it available for any possible 35 // re-entrant 36 // requests which may occur before the current request completes. 37 // SEC-1396. 38 securityContextRepository.saveContext(SecurityContextHolder.getContext(), 39 request, response); 40 } 41 else { 42 // No security context or authentication present. Check for a session 43 // timeout 44 if (request.getRequestedSessionId() != null 45 && !request.isRequestedSessionIdValid()) { 46 if (logger.isDebugEnabled()) { 47 logger.debug("Requested session ID " 48 + request.getRequestedSessionId() + " is invalid."); 49 } 50 51 if (invalidSessionStrategy != null) { 52 invalidSessionStrategy 53 .onInvalidSessionDetected(request, response); 54 return; 55 } 56 } 57 } 58 } 59 60 chain.doFilter(request, response); 61 }
2.2.9 /index.html at position 9 of 10 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
异常拦截,其处在Filter链后部分,只能拦截其后面的节点并且只处理AuthenticationException与AccessDeniedException两个异常。
AuthenticationException指的是未登录状态下访问受保护资源,AccessDeniedException指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 HttpServletRequest request = (HttpServletRequest) req; 4 HttpServletResponse response = (HttpServletResponse) res; 5 6 try { 7 // 直接执行后面的filter,并捕获异常 8 chain.doFilter(request, response); 9 10 logger.debug("Chain processed normally"); 11 } 12 catch (IOException ex) { 13 throw ex; 14 } 15 catch (Exception ex) { 16 // 从异常堆栈中提取SpringSecurityException 17 Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); 18 RuntimeException ase = (AuthenticationException) throwableAnalyzer 19 .getFirstThrowableOfType(AuthenticationException.class, causeChain); 20 21 if (ase == null) { 22 ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( 23 AccessDeniedException.class, causeChain); 24 } 25 26 if (ase != null) { 27 // 处理异常 28 handleSpringSecurityException(request, response, chain, ase); 29 } 30 else { 31 // Rethrow ServletExceptions and RuntimeExceptions as-is 32 if (ex instanceof ServletException) { 33 throw (ServletException) ex; 34 } 35 else if (ex instanceof RuntimeException) { 36 throw (RuntimeException) ex; 37 } 38 39 // Wrap other Exceptions. This shouldn't actually happen 40 // as we've already covered all the possibilities for doFilter 41 throw new RuntimeException(ex); 42 } 43 } 44 }
在这个catch代码中通过从异常堆栈中捕获到Throwable[],然后通过handleSpringSecurityException方法处理异常,在该方法中只会去处理AuthenticationException和AccessDeniedException异常。
1 private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { 2 3 if (exception instanceof AuthenticationException) { 4 // 认证异常,由sendStartAuthentication方法发起认证过程 5 logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception); 6 sendStartAuthentication(request, response, chain, (AuthenticationException) exception); 7 } else if (exception instanceof AccessDeniedException) { 8 // 访问权限异常 9 if (authenticationTrustResolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication())) { 10 logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point", exception); 11 // 匿名用户重定向到认证入口点执行认证过程 12 sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException("Full authentication is required to access this resource")); 13 } else { 14 // 拒绝访问,由accessDeniedHandler处理,response 403 15 logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); 16 accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); 17 } 18 } 19 } 20 21 protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { 22 // SEC-112: Clear the SecurityContextHolder's Authentication, as the existing Authentication is no longer considered valid 23 // 将SecurityContext中的Authentication置为null 24 SecurityContextHolder.getContext().setAuthentication(null); 25 // 在调用认证前先将request保存到session 26 requestCache.saveRequest(request, response); 27 logger.debug("Calling Authentication entry point."); 28 // 重定向到认证入口点执行认证 29 authenticationEntryPoint.commence(request, response, reason); 30 }
2.2.10 /index.html at position 10 of 10 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
这个filter用于授权验证。FilterSecurityInterceptor的工作流程引用一下,可以理解如下:FilterSecurityInterceptor从SecurityContextHolder中获取Authentication对象,然后比对用户拥有的权限和资源所需的权限。前者可以通过Authentication对象直接获得,而后者则需要引入我们之前一直未提到过的两个类:SecurityMetadataSource,AccessDecisionManager。
1 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 2 FilterInvocation fi = new FilterInvocation(request, response, chain); 3 invoke(fi); 4 } 5 6 7 public void invoke(FilterInvocation fi) throws IOException, ServletException { 8 if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { 9 // filter already applied to this request and user wants us to observe 10 // once-per-request handling, so don't re-do security checking 11 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 12 } else { 13 // first time this request being called, so perform security checking 14 if (fi.getRequest() != null) { 15 fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); 16 } 17 18 InterceptorStatusToken token = super.beforeInvocation(fi); 19 20 try { 21 // 如果后面还有filter则继续执行 22 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 23 } 24 finally { 25 // 保存securityContext 26 super.finallyInvocation(token); 27 } 28 29 super.afterInvocation(token, null); 30 } 31 } 32 33 protected InterceptorStatusToken beforeInvocation(Object object) { 34 Assert.notNull(object, "Object was null"); 35 final boolean debug = logger.isDebugEnabled(); 36 37 if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { 38 throw new IllegalArgumentException( 39 "Security invocation attempted for object " 40 + object.getClass().getName() 41 + " but AbstractSecurityInterceptor only configured to support secure objects of type: " 42 + getSecureObjectClass()); 43 } 44 45 // 获取配置的权限属性 46 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 47 48 if (attributes == null || attributes.isEmpty()) { 49 if (rejectPublicInvocations) { 50 throw new IllegalArgumentException( 51 "Secure object invocation " 52 + object 53 + " was denied as public invocations are not allowed via this interceptor. " 54 + "This indicates a configuration error because the " 55 + "rejectPublicInvocations property is set to 'true'"); 56 } 57 58 if (debug) { 59 logger.debug("Public object - authentication not attempted"); 60 } 61 62 publishEvent(new PublicInvocationEvent(object)); 63 64 return null; // no further work post-invocation 65 } 66 67 if (debug) { 68 logger.debug("Secure object: " + object + "; Attributes: " + attributes); 69 } 70 71 if (SecurityContextHolder.getContext().getAuthentication() == null) { 72 credentialsNotFound(messages.getMessage( 73 "AbstractSecurityInterceptor.authenticationNotFound", 74 "An Authentication object was not found in the SecurityContext"), 75 object, attributes); 76 } 77 78 // 获取Authentication,如果没有进行认证则认证后返回authentication 79 Authentication authenticated = authenticateIfRequired(); 80 81 // Attempt authorization 82 try { 83 // 使用voter决策是否拥有资源需要的权限 84 this.accessDecisionManager.decide(authenticated, object, attributes); 85 } catch (AccessDeniedException accessDeniedException) { 86 // 捕获到异常继续上抛 87 publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); 88 throw accessDeniedException; 89 } 90 91 if (debug) { 92 logger.debug("Authorization successful"); 93 } 94 95 if (publishAuthorizationSuccess) { 96 publishEvent(new AuthorizedEvent(object, attributes, authenticated)); 97 } 98 99 // Attempt to run as a different user 100 Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, 101 attributes); 102 103 if (runAs == null) { 104 if (debug) { 105 logger.debug("RunAsManager did not change Authentication object"); 106 } 107 108 // no further work post-invocation 109 return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, 110 attributes, object); 111 } 112 else { 113 if (debug) { 114 logger.debug("Switching to RunAs Authentication: " + runAs); 115 } 116 117 SecurityContext origCtx = SecurityContextHolder.getContext(); 118 SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); 119 SecurityContextHolder.getContext().setAuthentication(runAs); 120 121 // need to revert to token.Authenticated post-invocation 122 return new InterceptorStatusToken(origCtx, true, attributes, object); 123 } 124 }
参考文章:
https://blog.coding.net/blog/Explore-the-cache-request-of-Security-Spring
http://blog.didispace.com/xjf-spring-security-4/
http://blog.csdn.net/benjamin_whx/article/details/39204679