SpringSecurity 原理解析【3】:認證與受權
在上篇文章中提到構建SecurityFilterChain過程當中存在一個方法級別的過濾器:FilterSecurityInterceptor。該過濾器統一調用了認證和受權兩種功能,而Spring Security主要就作這2件事,1: 身份認證(誰在發起請求),2:身份受權(是否有權限訪問資源)。可是須要明確一點:FilterSecurityInterceptor主要作的是基於訪問規則的身份受權。而身份認證是身份受權的前提,所以FilterSecurityInterceptor會在認證信息不存在時進行一次身份認證。正常認證流程是在其餘優先級更高的過濾器完成的身份認證,固然兩者的認證流程一致:html
- 經過AuthenticationManager獲取當前請求的身份認證信息
- 經過AccessDecisionManager決斷特定訪問規則的web資源可否被訪問
身份認證
身份認證就是辨別出當前請求是誰發出的。在Spring Security中,哪怕不須要知道某個請求是誰發出的,也會給這個請求的來源構建一個身份信息:匿名身份。java
對於須要知道請求的身份信息的,則須要客戶端提供身份標識碼和開發者提供身份識別檔案信息,兩者比對以後才能作出究竟是哪一個具體身份的決斷。客戶端提供的身份標識被抽象爲令牌Token,提供身份檔案信息的方式抽象爲:認證提供者AuthenticationProvider。web
身份識別令牌
一個完整的身份識別令牌應該能展現如下信息:令牌所屬人:Principal、所屬人的身份認證憑證:Credentials、 所屬人附加信息:Details、所屬人的權限信息:Authorities。在Spring Security中使用Authentication表示安全
public interface Authentication extends Principal, Serializable { // 受權集合:GrantedAuthority實現類 Collection<? extends GrantedAuthority> getAuthorities(); // 憑證:【密碼】 Object getCredentials(); // 詳情:【其餘信息】 Object getDetails(); // 主體:【帳號】 Object getPrincipal(); // 是否已認證:true爲已認證 boolean isAuthenticated(); // 設置是否已認證: void setAuthenticated(boolean var1) throws IllegalArgumentException; }
客戶端不能提供完整的身份識別令牌,由於客戶端的信息並不可靠,所以通常而言客戶端不須要提供完整的令牌信息,只須要提供能識別出所屬人Principal的識別碼便可,剩餘的信息交給服務端去填充。Spring Security的身份認證過程就是對身份識別令牌的填充過程。app
全部的令牌都是Authentication的子類,令牌提供所屬人識別碼來填充完整令牌所屬人信息。根據令牌識別碼的提供方式不一樣,令牌實現也不一樣,常見提供方式有:帳號密碼、手機號、Cookie、Jwt、第三方受權碼、圖形驗證碼等等。而Spring Security中內置的令牌具備:RememberMeAuthenticationToken(靜默登陸令牌)、AnonymousAuthenticationToken(匿名訪問令牌)、UsernamePasswordAuthenticationToken(帳號密碼令牌)、PreAuthenticatedAuthenticationToken(提早認證令牌)、RunAsUserToken(身份轉換令牌)等ide
以帳號密碼令牌爲例
Spring Security對帳號密碼令牌的支持爲:UsernamePasswordAuthenticationToken,想使用該令牌進行身份識別須要在SecurityFilterChain中添加UsernamePasswordAuthenticationFilter。固然配置方式仍是在HttpSecurity處配置post
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 登陸頁不須要權限就能訪問 .antMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() // 使用UsernamePasswordAuthenticationToken做爲客戶端使用的身份識別令牌 .formLogin() // 登陸頁面 .loginPage("/login.html") // 進行身份識別匹配的路徑,這是一個POST方式的請求匹配器 .loginProcessingUrl("/doLogin") // 身份識別成功以後的重定向展現頁面 .defaultSuccessUrl("/home.html",false) // 身份識別失敗以後的重定向提示頁面 .failureUrl("/login.html?error=1") .and() .logout() ; }
不管何種令牌,都需指定進行認證操做的請求路徑:AuthenticationURL。在帳號密碼令牌中,該認證路徑使用loginProcessingUrl屬性配置,並默認爲POST方式的AntPathRequestMatcher。該匹配器在父類AbstractAuthenticationProcessingFilter中,經過requiresAuthentication來判斷是否須要認證。若是當前請求是匹配的認證路徑,則認證方法以下:ui
Authentication authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter實現簡化爲this
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 請求中獲取參數 String username = obtainUsername(request); String password = obtainPassword(request); // 構建令牌 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 設置Details setDetails(request, authRequest); // 認證入口:AuthenticationManager#authenticate return this.getAuthenticationManager().authenticate(authRequest); }
不管何種令牌,在初步構建以後都會交給AuthenticationManager#authenticate來完成認證。這裏就引入了Spring Security的身份認證核心對象:認證管理器:AuthenticationManager。能夠這麼理解:處理好了客戶端的非完整令牌,那麼服務端就須要來逐步完善這個令牌。認證管理器做爲總的管理者,統一管理入口。spa
ProviderManager
AuthenticationManager是一個接口規範,其實現類爲:ProviderManager,提供者管理器:管理能提供身份檔案信息的對象(AuthenticationProvider),能證實令牌確確實是屬於某個身份,能證實的方式有不少,Spring Security不是多方驗證,而是首次驗證成功便可,也就是說雖然有不少方式能證實令牌真的屬於誰,可是Spring Security只須要一個能提供證實身份證實的檔案便可。ProviderManager是一個父子分層結構,若是都不能證實則會去父管理器中去證實。ProviderManager主要結構以下
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... // 管理多個AuthenticationProvider(認證提供者) private List<AuthenticationProvider> providers = Collections.emptyList(); // 父管理器 private AuthenticationManager parent; ... }
AuthenticationProvider
AuthenticationProvider做爲一個能提供身份檔案信息的接口規範,主要規範認證功能,針對特定令牌提供supports功能,並且基於職責單一原則,每種Token都會有一個AuthenticationProvider實現。
public interface AuthenticationProvider { // Token令牌身份識別 Authentication authenticate(Authentication authentication) throws AuthenticationException; // 是否支持某種類型的Token令牌 boolean supports(Class<?> authentication); }
以帳號密碼認證提供者爲例
此種令牌是客戶端提供帳號、密碼來做爲識別碼進行身份識別的認證方式,所以服務端應該會有一個存儲大量帳號、密碼和其餘信息的地方。Spring Security將一個能存儲用戶認證信息的對象抽象爲:UserDetails。
public interface UserDetails extends Serializable { //受權集合 Collection<? extends GrantedAuthority> getAuthorities(); // 密碼 String getPassword(); //帳號 String getUsername(); //帳號是否過時 boolean isAccountNonExpired(); //認證前置校驗 //帳號是否鎖定 boolean isAccountNonLocked(); //認證前置校驗 // 憑證是否過時 boolean isCredentialsNonExpired(); //認證後置校驗 //帳號是否禁用 boolean isEnabled(); //認證前置校驗 }
認證提供者須要作的就是從存儲的庫中找到對應的UserDetails。DaoAuthenticationProvider就是經過數據訪問(Data Access Object)來獲取到認證對象的檔案信息的。獲取的實現交給了開發者,實現UserDetailsService#loadUserByUsername方法
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
獲取到UserDetails就能夠去認證了,流程簡化以下
public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 矯正帳戶名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); // 獲取認證憑證 UserDetails user = this.getUserDetailsService().loadUserByUsername(username); // 前置帳號檢測:是否上鎖:LockedException,是否禁用:DisabledException,是否失效:AccountExpiredException preAuthenticationChecks.check(user); // 主體檢測:抽象方法:默認爲密碼匹配檢測 additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); // 後置憑證檢測:憑證是否失效:CredentialsExpiredException postAuthenticationChecks.check(user); // 返回填充完整的令牌 return createSuccessAuthentication(principalToReturn, authentication, user); }
完整的令牌是UsernamePasswordAuthenticationToken的一個新的實例,各類認證信息齊全。
令牌完整後續處理
身份識別成以後就會獲得一個完整的令牌。後續則會處理Session(會話相關)、Security Context(上下文相關)、RememberMe(靜默登陸相關)、SuccessHandler(成功以後跳轉相關)
至此身份認證流程完畢,登陸成功以後通常都會根據SuccessHandler跳轉的固定頁面,從而開啓訪問受權決斷相關流程。認證流程圖示以下:
身份受權
身份認證成功就可以判斷令牌屬於誰,身份受權則判斷該身份可否訪問指定資源。在上篇文章說了SecurityFilterChain的構建來源,除了被忽略的認證和被其餘過濾器攔截的,剩下的基本都是基於安全訪問規則(ConfigAttribute)的判斷了。判斷入口在FilterSecurityInterceptor#invoke
安全訪問規則:ConfigAttribute
從代碼中收集安全訪問規則,主要存在如下類型
- WebExpressionConfigAttribute
基於Web表達式的訪問規則,目前只有一個來源:ExpressionUrlAuthorizationConfigurer,HttpSecurity默認使用的就是該類http.authorizeRequests() .antMatchers("/index").access("hasAnyRole('ANONYMOUS', 'USER')") .antMatchers("/login/*").access("hasAnyRole('ANONYMOUS', 'USER')")
- SecurityConfig
基於配置類的訪問規則,常規用法基本都是該對象,例如@Secured、@PermitAll、@DenyAll 和 UrlAuthorizationConfigurer(HttpSecurity可配置)@GetMapping("/{id}") @PermitAll() public Result<Integer> find(@PathVariable Long id) { return Result.success(service.find(id)); }
- PreInvocationExpressionAttribute
基於AOP前置通知表達式的訪問規則,主要是對@PreFilter 和 @PreAuthorize,這也是最經常使用的@DeleteMapping("/{id}") @PreAuthorize("hasAuthority('del')") public Result<Boolean> deleteById(@PathVariable Long id) { return Result.success(service.deleteById(id)); }
- PostInvocationExpressionAttribute
基於AOP後置通知調用表達式的訪問規則,主要是對@PostFilter 和 @PostAuthorize@GetMapping("/{id}") @PostAuthorize("returnObject.data%2==0") public Result<Integer> find(@PathVariable Long id) { return Result.success(service.find(id)); }
受權模型
爲了便於開發,設計的安全訪問規則來源有好幾種,不一樣的安全訪問規則須要不一樣的處理機制來解析。能對某種安全訪問規則作出當前請求可否經過該規則並訪問到資源的決斷的對象在Spring Security中抽象爲AccessDecisionVoter:選民,作出決斷的動做稱爲:vote:投票。AccessDecisionVoter只能對支持的安全訪問規則作出同意、反對、棄權之一的決斷,每一個決斷1個權重。接口規範以下:
public interface AccessDecisionVoter<S> { // 同意票 int ACCESS_GRANTED = 1; // 棄權票 int ACCESS_ABSTAIN = 0; // 反對票 int ACCESS_DENIED = -1; /** * 是否支持對某配置數據進行投票 */ boolean supports(ConfigAttribute attribute); /** * 是否支持對某類型元數據進行投票 */ boolean supports(Class<?> clazz); /** * 對認證信息進行投票 */ int vote(Authentication authentication, S object,Collection<ConfigAttribute> attributes); }
Spring Security中對安全訪問規則的最終決斷是基於投票結果真後根據決策方針才作出的結論。能作出最終決斷的接口爲:AccessDecisionManager。
// 作出最終決斷的方法# void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
抽象實現爲AbstractAccessDecisionManager,他管理多個選民AccessDecisionVoter
private List<AccessDecisionVoter<?>> decisionVoters;
AbstractAccessDecisionManager,具體類爲決策方針,目前有三大方針,默認爲:一票經過方針:AffirmativeBased
決策方針
AffirmativeBased【一票經過方針】
public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { ... for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { // 一票贊成 case AccessDecisionVoter.ACCESS_GRANTED: return; ... } }
ConsensusBased【少數服從多數方針】
public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { ... for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: grant++; break; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } // 多數贊成纔有效 if (grant > deny) { return; } // 少數贊成,決策無效 if (deny > grant) { throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // 票數相等則根據allowIfEqualGrantedDeniedDecisions來決定是否經過 if ((grant == deny) && (grant != 0)) { if (this.allowIfEqualGrantedDeniedDecisions) { return; } else { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } } ... }
UnanimousBased【一票否決方針】
public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> attributes) throws AccessDeniedException { ... for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, singleAttributeList); switch (result) { ... // 一票否決 case AccessDecisionVoter.ACCESS_DENIED: throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); ... } ... }
選民類型
基於角色的選民【RoleVoter】
針對具備角色權限標識的安全訪問規則進行投票,角色權限標識特徵:private String rolePrefix = "ROLE_";
,在Spring Security中角色權限都是該字符前綴,當對用戶是否擁有該角色權限時,就須要經過RoleVoter進行投票,注:前綴可配置
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { if (authentication == null) { return ACCESS_DENIED; } int result = ACCESS_ABSTAIN; // 從Token令牌中提取已受權集合 Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); // 先判斷選民是否支持對該屬性進行投票 for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { result = ACCESS_DENIED; // 當支持時,則從已受權集合中獲取到該權限標識,若是獲取不到則表示無權限,投反對票 // 已受權集合在身份認證時已獲取 for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; }
基於角色分級的選民【RoleHierarchyVoter】
在RoleVoter基礎上,將角色分級,高級別的角色將自動擁有低級別角色的權限,使用方式爲角色名稱之間經過大於號「>」分割,前面的角色自動擁有後面角色的權限
@Override Collection<? extends GrantedAuthority> extractAuthorities( Authentication authentication) { // 提取權限集合時,會將低級別角色的權限也獲取到 return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); }
分割方法
String[] roles = line.trim().split("\\s+>\\s+");
基於認證令牌的選民【AuthenticatedVoter】
針對固定的權限標識進行投票,這種標識有三個:全權受權:IS_AUTHENTICATED_FULLY
、RememberMe受權
IS_AUTHENTICATED_REMEMBERED
、匿名訪問受權
IS_AUTHENTICATED_ANONYMOUSLY
。可是每種權限須要指定的Token才能投同意票,這裏引入了AuthenticationTrustResolver,主要就是判斷是否爲Anonymous或者RememberMe
IS_AUTHENTICATED_REMEMBERED:須要認證令牌爲:RememberMeAuthenticationToken或者其子類(AuthenticationTrustResolver默認實現),才能對該標識投同意票
IS_AUTHENTICATED_ANONYMOUSLY:須要認證令牌爲:AnonymousAuthenticationToken或者其子類(AuthenticationTrustResolver默認實現),才能對該標識投同意票
IS_AUTHENTICATED_FULLY:須要認證令牌不是以上兩種令牌,才能對該標識投同意票
基於Web表達式的選民【WebExpressionVoter】
針對於Web表達式的權限控制,表達式的解析處理器爲SecurityExpressionHandler,解析時使用的是SPEL解析器:SpelExpressionParser。
SecurityExpressionHandler能對以上全部投票方式進行解析,解析結果爲Boolean,true則表示同意,false則表示反對
// 只支持Web表達式的屬性 public boolean supports(ConfigAttribute attribute) { return attribute instanceof WebExpressionConfigAttribute; } // 只支持FilterInvocation類型 public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); }
前置調用加強的投票者【PreInvocationAuthorizationAdviceVoter】
一樣是基於SPEL表達式,但該表達式只針對方法級別的@PreFilter 和 @PreAuthorize,解析器爲:MethodSecurityExpressionHandler。
public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) { // @PreFilter 和 @PreAuthorize加強獲取 PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes); if (preAttr == null) { return ACCESS_ABSTAIN; } // EL表達式解析 boolean allowed = preAdvice.before(authentication, method, preAttr); // 解析結果投票 return allowed ? ACCESS_GRANTED : ACCESS_DENIED; }
基於Jsr250規範的投票者【Jsr250Voter】
針對Jsr250規範的註解@PermitAll和@DenyAll控制的權限,前置投同意票,後置投反對票。對於supports的權限投同意票
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> definition) { boolean jsr250AttributeFound = false; for (ConfigAttribute attribute : definition) { //@PermitAll 同意 if (Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE.equals(attribute)) { return ACCESS_GRANTED; } //@DenyAll 反對 if (Jsr250SecurityConfig.DENY_ALL_ATTRIBUTE.equals(attribute)) { return ACCESS_DENIED; } //supports if (supports(attribute)) { jsr250AttributeFound = true; // 若是已受權則投同意票 for (GrantedAuthority authority : authentication.getAuthorities()) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } // 未受權可是support的投反對票,未support的棄權 return jsr250AttributeFound ? ACCESS_DENIED : ACCESS_ABSTAIN; }
以上就是Spring Security對受權功能的實現,若是決斷的最終結果是經過,則Filter會繼續執行下去,不然會拋出異常。受權總體流程以下圖