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
