您好,登錄后才能下訂單哦!
本篇內容主要講解“SpringSecurity原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“SpringSecurity原理是什么”吧!
在上篇文章中提到構建SecurityFilterChain過程中存在一個方法級別的過濾器:FilterSecurityInterceptor。該過濾器統一調用了認證和授權兩種功能,而Spring Security主要就做這2件事,1: 身份認證(誰在發起請求),2:身份授權(是否有權限訪問資源)。但是需要明確一點:FilterSecurityInterceptor主要做的是基于訪問規則的身份授權。而身份認證是身份授權的前提,因此FilterSecurityInterceptor會在認證信息不存在時進行一次身份認證。正常認證流程是在其他優先級更高的過濾器完成的身份認證,當然二者的認證流程一致:
通過AuthenticationManager獲取當前請求的身份認證信息
通過AccessDecisionManager決斷特定訪問規則的web資源能否被訪問
身份認證就是辨別出當前請求是誰發出的。在Spring Security中,哪怕不需要知道某個請求是誰發出的,也會給這個請求的來源構建一個身份信息:匿名身份。
對于需要知道請求的身份信息的,則需要客戶端提供身份標識碼和開發者提供身份識別檔案信息,二者比對之后才能做出到底是哪個具體身份的決斷。客戶端提供的身份標識被抽象為令牌Token,提供身份檔案信息的方式抽象為:認證提供者AuthenticationProvider。
一個完整的身份識別令牌應該能展示以下信息:令牌所屬人: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的身份認證過程就是對身份識別令牌的填充過程。
所有的令牌都是Authentication的子類,令牌提供所屬人識別碼來填充完整令牌所屬人信息。根據令牌識別碼的提供方式不同,令牌實現也不同,常見提供方式有:賬號密碼、手機號、Cookie、Jwt、第三方授權碼、圖形驗證碼等等。而Spring Security中內置的令牌具有:RememberMeAuthenticationToken(靜默登錄令牌)、AnonymousAuthenticationToken(匿名訪問令牌)、UsernamePasswordAuthenticationToken(賬號密碼令牌)、PreAuthenticatedAuthenticationToken(提前認證令牌)、RunAsUserToken(身份轉換令牌)等
Spring Security對賬號密碼令牌的支持為:UsernamePasswordAuthenticationToken,想使用該令牌進行身份識別需要在SecurityFilterChain中添加UsernamePasswordAuthenticationFilter。當然配置方式還是在HttpSecurity處配置
@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來判斷是否需要認證。如果當前請求是匹配的認證路徑,則認證方法如下:
Authentication authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter實現簡化為
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。可以這么理解:處理好了客戶端的非完整令牌,那么服務端就需要來逐步完善這個令牌。認證管理器作為總的管理者,統一管理入口。
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作為一個能提供身份檔案信息的接口規范,主要規范認證功能,針對特定令牌提供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
從代碼中收集安全訪問規則,主要存在以下類型
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
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; ... } }
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")); } } ... }
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")); ... } ... }
決策方針的執行需要選民參與投票,三大決策都是通過遍歷decisionVoters來統計vote結果的。不同的decisionVoter解析不同的安全訪問規則,因此當一個訪問規則需要決斷時,只有支持當前訪問規則的decisionVoter才能做出決斷。因此需要知道一個訪問規則會被哪種decisionVoter解析
針對具有角色權限標識的安全訪問規則進行投票,角色權限標識特征: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; }
在RoleVoter基礎上,將角色分級,高級別的角色將自動擁有低級別角色的權限,使用方式為角色名稱之間通過大于號“>”分割,前面的角色自動擁有后面角色的權限
@Override Collection<? extends GrantedAuthority> extractAuthorities( Authentication authentication) { // 提取權限集合時,會將低級別角色的權限也獲取到 return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()); }
分割方法
String[] roles = line.trim().split("\\s+>\\s+");
針對固定的權限標識進行投票,這種標識有三個:全權授權: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表達式的權限控制,表達式的解析處理器為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); }
同樣是基于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規范的注解@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會繼續執行下去,否則會拋出異常。授權整體流程如下圖
到此,相信大家對“SpringSecurity原理是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。