參考以下文章整理的筆記:web
#1 Authentication認證信息spring
認證前:用來表示用戶輸入的認證信息的對象安全
認證後:包含用戶詳細信息以及權限信息的對象cookie
接口定義以下:session
public interface Authentication extends Principal, Serializable { //獲取用戶權限 Collection<? extends GrantedAuthority> getAuthorities(); //獲取密碼信息 Object getCredentials(); Object getDetails(); //獲取用戶惟一標示,如用戶名 Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
接口實現以下:app
#2 SecurityContextHolder用來保存SecurityContext信息的jsp
SecurityContextHolder默認使用ThreadLocalSecurityContextHolderStrategy策略來保存。ide
ThreadLocalSecurityContextHolderStrategy策略實現就是使用ThreadLocal來保存post
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>(); }
SecurityContext又是用來保存上述Authentication信息,接口定義以下:ui
public interface SecurityContext extends Serializable { Authentication getAuthentication(); void setAuthentication(Authentication authentication); }
#3 AuthenticationManager對用戶輸入信息進行認證
接口定義以下:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
接口實現以下:
默認實現ProviderManager,內部包含了一個List類型的AuthenticationProvider providers,也就是說ProviderManager委託內部的AuthenticationProvider來實現認證的
使用以下標籤默認就是建立ProviderManager:
<security:authentication-manager alias="authenticationManager"> <security:authentication-provider user-service-ref="userDetailsService"/> <security:authentication-provider ref="xxxxx"/> </security:authentication-manager>
#4 AuthenticationProvider對用戶輸入信息進行認證
接口的定義以下:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication)throws AuthenticationException; boolean supports(Class<?> authentication); }
接口實現以下:
默認實現DaoAuthenticationProvider:
內部含有一個UserDetailsService userDetailsService對象,用來加載用戶信息包含權限信息
1 使用UserDetailsService userDetailsService來加載用戶信息到UserDetails對象中
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
所以在UserDetailsService上述加載過程實現中,咱們就能夠根據用戶名來判斷用戶是否存在。
UserDetails接口實現,默認就是org.springframework.security.core.userdetails.User,因此咱們通常須要繼承該對象
2 檢查上述用戶信息是否過時、被鎖定等等,分紅preAuthenticationChecks,檢查密碼是否正確和postAuthenticationChecks
private class DefaultPreAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isAccountNonLocked()) { logger.debug("User account is locked"); throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), user); } if (!user.isEnabled()) { logger.debug("User account is disabled"); throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"), user); } if (!user.isAccountNonExpired()) { logger.debug("User account is expired"); throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), user); } } } protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails); } }
其中上述密碼檢查,接口是PasswordEncoder,默認是PlaintextPasswordEncoder,沒有加密過的
private class DefaultPostAuthenticationChecks implements UserDetailsChecker { public void check(UserDetails user) { if (!user.isCredentialsNonExpired()) { logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"), user); } } }
3 一旦認證經過,構建一個UsernamePasswordAuthenticationToken對象,使用上述UserDetails信息、用戶輸入的密碼信息
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
#5 認證過程
ExceptionTranslationFilter是用來處理來自AbstractSecurityInterceptor拋出的AuthenticationException和AccessDeniedException的
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException)throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } if (ase != null) { handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } }
AbstractSecurityInterceptor是Spring Security用於攔截請求進行權限鑑定的,其擁有兩個具體的子類,攔截方法調用的MethodSecurityInterceptor和攔截URL請求的FilterSecurityInterceptor。
當ExceptionTranslationFilter捕獲到的是AuthenticationException時將調用AuthenticationEntryPoint引導用戶進行登陸;
若是捕獲的是AccessDeniedException,可是用戶尚未經過認證,則調用AuthenticationEntryPoint引導用戶進行登陸認證,不然將返回一個表示不存在對應權限的403錯誤碼
1 AbstractSecurityInterceptor對用戶訪問資源進行檢查,若是未認證或者沒權限則拋出異常
2 ExceptionTranslationFilter這個Filter捕獲上述異常信息,引導重定向到登錄界面
3 用戶輸入用戶名和密碼進行認證登錄
4 Spring Security將獲取到的用戶名和密碼封裝成一個實現了Authentication接口的UsernamePasswordAuthenticationToken
5 將上述產生的token對象傳遞給AuthenticationManager進行登陸認證。
6 AuthenticationManager認證成功後將會返回一個封裝了用戶權限等信息的Authentication對象
7 將上述Authentication對象封裝到SecurityContext中
8 SecurityContextPersistentFilter將上述SecurityContext保存到對應的HttpSession屬性中,key爲:SPRING_SECURITY_CONTEXT
SecurityContextPersistentFilter在Filter執行前,從HttpSession中先嚐試獲取保存的SecurityContext對象信息,若是沒有則建立一個。若是HttpSession沒有的話,看SecurityContextPersistentFilter的forceEagerSessionCreation屬性,若是強制產生,則在此處調用request的getSession方法產生HttpSession
Filter執行後,會將上述SecurityContext對象信息保存到HttpSession的屬性中
所以,一樣用戶再次訪問的時候,就不須要進行登陸認證了,只須要從HttpSession中就能取出對應的認證信息SecurityContext了
也就是說,用戶在某一次請求的過程當中,SecurityContext是保存在ThreadLocal中的,可是請求結束後,就會被清除了。同時,SecurityContext也被保存在HttpSession中,這樣的話,同一個用戶不一樣請求即便是在不一樣的線程上,也都能根據session來獲取到SecurityContext信息。
#6 Filter鏈
Spring Security已經定義了一些Filter,無論實際應用中你用到了哪些,它們應當保持以下順序。
在web.xml中以下配置:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
DelegatingFilterProxy實際上是委託給Spring容器中的Filter來執行,如何指定哪個呢?採用DelegatingFilterProxy自身的targetBeanName這個參數
上述就是默認取值spring容器中的這個springSecurityFilterChain Filter,採用的是FilterChainProxy類
當咱們使用基於Spring Security的NameSpace進行配置時,系統會自動爲咱們註冊一個名爲springSecurityFilterChain類型爲FilterChainProxy的bean。該FilterChainProxy含有一個List<SecurityFilterChain> filterChains屬性,咱們以下配置
Spring security容許咱們在配置文件中配置多個http元素,以針對不一樣形式的URL使用不一樣的安全控制。Spring Security將會爲每個http元素建立對應的FilterChain,同時按照它們的聲明順序加入到FilterChainProxy。因此當咱們同時定義多個http元素時要確保將更具備特性的URL配置在前。
<security:http pattern="/login*.jsp*" security="none"/> <!-- http元素的pattern屬性指定當前的http對應的FilterChain將匹配哪些URL,如未指定將匹配全部的請求 --> <security:http pattern="/admin/**"> <security:intercept-url pattern="/**" access="ROLE_ADMIN"/> </security:http> <security:http> <security:intercept-url pattern="/**" access="ROLE_USER"/> </security:http>
每個security:http都將對應一個SecurityFilterChain。每個SecurityFilterChain中都包含各自的Filter
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall.getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall.getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); }
默認註冊了以下Filter
SecurityContextPersistenceFilter
在一開始進行request的時候就能夠在SecurityContextHolder中創建一個SecurityContext,而後在請求結束的時候,任何對SecurityContext的改變均可以被copy到HttpSession
LogoutFilter
檢查是不是登出地址
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter會檢查請求地址是否是配置的登錄地址,若是是則進行認證檢查,從request中獲取username和password構建成UsernamePasswordAuthenticationToken,而後使用AuthenticationManager authenticationManager來進行認證過程,認證成功以後重定向到target地址
BasicAuthenticationFilter
RequestCacheAwareFilter
session中還能夠保存着key爲SPRING_SECURITY_SAVED_REQUEST的類型爲DefaultSavedRequest的request信息,每次請求都要從request中獲取session,若是沒有則不會建立session,若是有session,則從session中獲取DefaultSavedRequest的request信息,若是沒有保存的話,則仍然爲null,默認狀況下,這個filter不起做用,由於沒有向session中保存上述信息
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
若是當前用戶的SecurityContext中的認證信息爲null的話,建立一個默認的角色
SessionManagementFilter
ExceptionTranslationFilter
捕獲以後程序執行中的AuthenticationException和AccessDeniedException。若是用戶沒有認證信息,則引導用戶重定向到登錄界面,若是已經有登錄信息,則認爲是權限不足,重定向到權限不足界面
FilterSecurityInterceptor
保護每個受訪問的uri,一旦用戶權限不足,則拋出AccessDeniedException異常
咱們能夠看到整個過程與session打交道的時候僅僅是在SecurityContextPersistenceFilter中,請求完成以後,會將SecurityContext信息保存到session中而已。供下次使用的時候,直接從session中獲取SecurityContext信息,此時的SecurityContext信息中含有用戶的認證信息,所以訪問一個資源的話,上述filter都不會攔截,在FilterSecurityInterceptor中也會被正常經過。
一旦session失效,不能從session中獲取SecurityContext信息了,就會在FilterSecurityInterceptor拋出AccessDeniedException異常,而後被ExceptionTranslationFilter捕獲,引導用戶重定向到登錄界面