參考文章
spring security 極客學(xué)院
spring security 博客園
Spring security 基本流程
Java配置的方式
spring security 大佬博客
spring security CSDN
基于XML配置
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd">
<http security="none" pattern="/resources/**"/>
<http security="none" pattern="/lib/**"/>
<http access-decision-manager-ref="accessDecisionManager">
<csrf request-matcher-ref="csrfSecurityRequestMatcher" />
<!--<csrf disabled="true"/>-->
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/timeout" access="permitAll" />
<intercept-url pattern="/login.html" access="permitAll" />
<intercept-url pattern="/verifiCode" access="permitAll" />
<intercept-url pattern="/common/**" access="permitAll" />
<intercept-url pattern="/websocket/**" access="permitAll" />
<intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<access-denied-handler error-page="/access-denied"/>
<session-management invalid-session-url="/timeout"/>
<form-login login-page='/login' authentication-success-handler-ref="successHandler"
authentication-failure-handler-ref="loginFailureHandler"/>
<!--authentication-failure-url="/login?error=true"/>-->
<!-- 驗證碼攔截器 -->
<custom-filter ref="captchaVerifierFilter" before="FORM_LOGIN_FILTER"/>
<logout logout-url="/logout" success-handler-ref="logoutHandler"/>
<headers defaults-disabled="true">
<cache-control/>
</headers>
</http>
<!-- 認(rèn)證管理器,確定用戶,角色及相應(yīng)的權(quán)限 -->
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<!-- 投票器 -->
<beans:constructor-arg>
<beans:list>
<beans:bean class="com.hand.hap.security.CustomWebExpressionVoter"/>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
<beans:bean class="com.hand.hap.security.PermissionVoter"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!--<beans:import resource="standardSecurity-LDAP.xml"/>-->
<authentication-manager>
<!--Ldap驗證-->
<!-- <authentication-provider ref="ldapAuthProvider" />-->
<!--標(biāo)準(zhǔn)登錄驗證-->
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder ref="passwordManager"/>
</authentication-provider>
</authentication-manager>
<beans:bean id="captchaVerifierFilter" class="com.hand.hap.security.CaptchaVerifierFilter">
<beans:property name="captchaField" value="verifiCode"/>
</beans:bean>
<beans:bean id="successHandler" class="com.hand.hap.security.CustomAuthenticationSuccessHandler">
<!-- <beans:property name="defaultTargetUrl" value="/index"/>-->
</beans:bean>
<beans:bean id="loginFailureHandler" class="com.hand.hap.security.LoginFailureHandler"/>
<beans:bean id="logoutHandler" class="com.hand.hap.security.CustomLogoutSuccessHandler"></beans:bean>
<beans:bean id="csrfSecurityRequestMatcher" class="com.hand.hap.security.CsrfSecurityRequestMatcher">
<beans:property name="excludeUrls">
<beans:list>
<beans:value>/login</beans:value>
<beans:value>/websocket/**</beans:value>
<beans:value>/ureport/**</beans:value>
</beans:list>
</beans:property>
</beans:bean>
</beans:beans>
基于Java配置
package com.hand.sxy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author spilledyear
* @date 2018/4/24 13:19
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
默認(rèn)驗證
當(dāng)我們項目里添加spring security依賴它就已經(jīng)起作用了唯沮,啟動項目訪問時,會出現(xiàn)彈出框。spring security默認(rèn)采用basic模式認(rèn)證介蛉。瀏覽器發(fā)送http報文請求一個受保護(hù)的資源萌庆,瀏覽器會彈出對話框讓輸入用戶名和密碼。并以用
戶名:密碼的形式base64加密甘耿,加入Http報文頭部的Authorization(默認(rèn)用戶名為user踊兜,密碼則是會在啟動程序時后
臺console里輸出,每次都不一樣)佳恬。后臺獲取Http報文頭部相關(guān)認(rèn)證信息捏境,認(rèn)證成功返回相應(yīng)內(nèi)容,失敗則繼續(xù)認(rèn)證毁葱。
基本概念
- AuthenticationManager: 身份驗證的主要策略設(shè)置接口垫言。
- ProviderManager: AuthenticationManager接口的最常用實現(xiàn)類。
- AuthenticationProvider: 是一個接口倾剿,ProviderManager委托列表中AuthenticationProvider處理認(rèn)證工作筷频。
- Authentication: 認(rèn)證用戶信息主體。
- GrantedAuthority: 用戶主體的權(quán)限前痘。
- UserDetails: 【接口】凛捏,通過自定義實現(xiàn)封裝用戶的基本必要信息。
- UserDetailsService: 【接口】芹缔,自定義的實現(xiàn)類通過loadUserByUsername方法返回一個UserDetails對象坯癣。
- SecurityContextHolder: 提供訪問SecurityContext。
- SecurityContext: 保存Authentication最欠,和一些其它的信息示罗。
ProviderManager把工作委托給AuthenticationProvider集合,對所有AuthenticationProvider進(jìn)行循環(huán)芝硬,直到運行返回一個完整的Authentication蚜点,不符合條件或者不能認(rèn)證當(dāng)前Authentication,返回AuthenticationException異嘲枰酰或者null绍绘。
核心過濾器
// 每個日志前面自動加上這個
2018-05-16 13:43:56.700 DEBUG 14448 --- [nio-8081-exec-1] o.s.security.web.FilterChainProxy
: /error at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
: /error at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
: /error at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
: /error at position 4 of 12 in additional filter chain; firing Filter: 'LogoutFilter'
: /error at position 5 of 12 in additional filter chain; firing Filter: 'JwtAuthorizationTokenFilter'
: /error at position 6 of 12 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
: /error at position 7 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
: /error at position 8 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
: /error at position 9 of 12 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
o.s.s.w.a.AnonymousAuthenticationFilter : Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@1dcfc3ac: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
: /error at position 10 of 12 in additional filter chain; firing Filter: 'SessionManagementFilter'
: /error at position 11 of 12 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
: /error at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
以上內(nèi)容是我格式化之后的日志,代碼的順序不變皮官「梗可以看到一共有12個過濾器,其中 第五個 過濾器 JwtAuthorizationTokenFilter 是我自定義的捺氢,其余的全是Spring Security自帶的藻丢。也就是說,Spring Security中默認(rèn)有 11 個過濾器摄乒。
- SecurityContextPersistenceFilter 兩個主要職責(zé):請求來臨時悠反,創(chuàng)建SecurityContext安全上下文信息残黑,請求結(jié)束時清空SecurityContextHolder。
- HeaderWriterFilter (文檔中并未介紹斋否,非核心過濾器) 用來給http響應(yīng)添加一些Header,比如X-Frame-Options, X-XSS-Protection*梨水,X-Content-Type-Options.
- CsrfFilter 在spring4這個版本中被默認(rèn)開啟的一個過濾器,用于防止csrf攻擊茵臭,了解前后端分離的人一定不會對這個攻擊方式感到陌生疫诽,前后端使用json交互需要注意的一個問題。
- LogoutFilter 顧名思義旦委,處理注銷的過濾器
- UsernamePasswordAuthenticationFilter 表單提交了username和password奇徒,被封裝成token進(jìn)行一系列的認(rèn)證,便是主要通過這個過濾器完成的缨硝,在表單認(rèn)證的方法中摩钙,這是最最關(guān)鍵的過濾器。
- RequestCacheAwareFilter (文檔中并未介紹查辩,非核心過濾器) 內(nèi)部維護(hù)了一個RequestCache胖笛,用于緩存request請求
- SecurityContextHolderAwareRequestFilter 此過濾器對ServletRequest進(jìn)行了一次包裝,使得request具有更加豐富的API
- AnonymousAuthenticationFilter 匿名身份過濾器宜岛,這個過濾器個人認(rèn)為很重要长踊,需要將它與UsernamePasswordAuthenticationFilter 放在一起比較理解,spring security為了兼容未登錄的訪問萍倡,也走了一套認(rèn)證流程之斯,只不過是一個匿名的身份。
- SessionManagementFilter 和session相關(guān)的過濾器遣铝,內(nèi)部維護(hù)了一個SessionAuthenticationStrategy,兩者組合使用莉擒,常用來防止session-fixation protection attack酿炸,以及限制同一用戶開啟多個會話的數(shù)量
- ExceptionTranslationFilter 直譯成異常翻譯過濾器,還是比較形象的涨冀,這個過濾器本身不處理異常填硕,而是將認(rèn)證過程中出現(xiàn)的異常交給內(nèi)部維護(hù)的一些類去處理,具體是那些類下面詳細(xì)介紹
- FilterSecurityInterceptor 這個過濾器決定了訪問特定路徑應(yīng)該具備的權(quán)限鹿鳖,訪問的用戶的角色扁眯,權(quán)限是什么?訪問的路徑需要什么樣的角色和權(quán)限翅帜?這些判斷和處理都是由該類進(jìn)行的姻檀。其實這個真的非常非常重要,前面的東西都是和用戶認(rèn)證相關(guān)涝滴,而這個是控制哪些資源是受限的绣版,這些受限的資源需要什么權(quán)限胶台,需要什么角色。
其中加粗的過濾器可以被認(rèn)為是Spring Security的核心過濾器杂抽。在日志中未發(fā)現(xiàn) CsrfFilter 诈唬,是因為我在代碼中把 csrf保護(hù)關(guān)閉了。
FilterSecurityInterceptor的工作流程可以理解如下:FilterSecurityInterceptor從SecurityContextHolder中獲取Authentication對象缩麸,然后比對用戶擁有的權(quán)限和資源所需的權(quán)限铸磅。前者可以通過Authentication對象直接獲得,而后者則需要引入兩個類:SecurityMetadataSource杭朱,AccessDecisionManager阅仔。
認(rèn)證流程
AbstractAuthenticationProcessingFilter
用于攔截認(rèn)證請求,它是基于瀏覽器和 HTTP 認(rèn)證請求的處理器痕檬,可以理解為它就是 Spring Security 認(rèn)證流程的入口霎槐。
整個認(rèn)證流程如下:
① AbstractAuthenticationProcessingFilter
收集用于認(rèn)證的用戶身份信息(通常是用戶名和密碼),并基于這些信息構(gòu)造一個 Authentication
請求對象梦谜,AbstractAuthenticationProcessingFilter
只是一個虛類丘跌,查看 Spring Security API 文檔 可以看到 Spring Security 提供了幾個實現(xiàn)類:
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
OpenIDAuthenticationFilter
UsernamePasswordAuthenticationFilter
最常使用的應(yīng)該是 UsernamePasswordAuthenticationFilter
,其它類都應(yīng)用于特定的場景唁桩。
② AbstractAuthenticationProcessingFilter
類將構(gòu)造的 Authentication
請求對象呈現(xiàn)給 AuthenticationManager
闭树,AbstractAuthenticationProcessingFilter
類有以下方法設(shè)置和獲取 AuthenticationManager
:
protected AuthenticationManager getAuthenticationManager()
public void setAuthenticationManager(AuthenticationManager authenticationManager)
③ AuthenticationManager
只是一個接口,Spring Security 提供了一個默認(rèn)實現(xiàn) ProviderManager
荒澡。ProviderManager
在接收到 AbstractAuthenticationProcessingFilter
傳遞過來的 Authentication
請求對象后并不會執(zhí)行認(rèn)證處理报辱,它持有一個 AuthenticationProvider
的列表,ProviderManager
委托列表中的 AuthenticationProvider
處理認(rèn)證請求单山;
④ AuthenticationProvider
也只是接口碍现,Spring Security 提供了很多此接口的實現(xiàn),如 DaoAuthenticationProvider
米奸、LdapAuthenticationProvider
昼接、JaasAuthenticationProvider
等,現(xiàn)在暫時不關(guān)心這些具體實現(xiàn)悴晰。列表中的 AuthenticationProvider
會依次對 Authentication
請求對象進(jìn)行認(rèn)證處理慢睡,如果認(rèn)證通過則返回一個完全填充的 Authentication
對象(后面會解釋什么是“完全填充”),如果認(rèn)證不通過則拋出一個異常(注意對拋出的異常有類型要求)或直接返回 null铡溪。如果列表中的所有 AuthenticationProvider
都返回 null漂辐,則 ProviderManager
會拋出 ProviderNotFoundException
異常;
⑤ 認(rèn)證通過后 AuthenticationProvider
返回完全填充的 Authentication
對象給 ProviderManager
棕硫,ProviderManager
繼續(xù)向上返回給 AbstractAuthenticationProcessingFilter
髓涯,AbstractAuthenticationProcessingFilter
會繼續(xù)返回。
⑥ Spring Security 的“authentication mechanism”在接收到一個完全填充的 Authentication
對象返回后會認(rèn)定認(rèn)證請求有效饲帅,并將此 Authentication
對象放入 SecurityContextHolder
。
⑦ SecurityContextHolder
是 Spring Security 最基礎(chǔ)的對象,用于存儲應(yīng)用程序當(dāng)前安全上下文的詳細(xì)信息键痛,這些信息后續(xù)會被用于授權(quán)。
核心組件
這一節(jié)主要介紹一些在Spring Security中常見且核心的Java類对途,它們之間的依賴,構(gòu)建起了整個框架髓棋。想要理解整個架構(gòu)实檀,最起碼得對這些類眼熟。
SecurityContextHolder
SecurityContextHolder
用于存儲安全上下文(security context)的信息按声。當(dāng)前操作的用戶是誰膳犹,該用戶是否已經(jīng)被認(rèn)證,他擁有哪些角色權(quán)限…這些都被保存在SecurityContextHolder中签则。SecurityContextHolder
默認(rèn)使用 ThreadLocal
策略來存儲認(rèn)證信息须床。看到ThreadLocal
也就意味著渐裂,這是一種與線程綁定的策略豺旬。Spring Security在用戶登錄時自動綁定認(rèn)證信息到當(dāng)前線程,在用戶退出時柒凉,自動清除當(dāng)前線程的認(rèn)證信息族阅。但這一切的前提,是你在web場景下使用Spring Security膝捞,而如果是Swing界面坦刀,Spring也提供了支持,SecurityContextHolder
的策略則需要被替換蔬咬,鑒于我的初衷是基于web來介紹Spring Security鲤遥,所以這里以及后續(xù),非web的相關(guān)的內(nèi)容都一筆帶過林艘。
獲取當(dāng)前用戶的信息
因為身份信息是與線程綁定的渴频,所以可以在程序的任何地方使用靜態(tài)方法獲取用戶信息。一個典型的獲取當(dāng)前登錄用戶的姓名的例子如下所示
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()返回了認(rèn)證信息北启,再次getPrincipal()返回了身份信息,UserDetails便是Spring對身份信息封裝的一個接口拔第。Authentication和UserDetails的介紹在下面的小節(jié)具體講解咕村,本節(jié)重要的內(nèi)容是介紹SecurityContextHolder這個容器。
Authentication
先看看這個接口的源碼長什么樣:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection<? extends GrantedAuthority> getAuthorities(); // <2>
Object getCredentials();// <2>
Object getDetails();// <2>
Object getPrincipal();// <2>
boolean isAuthenticated();// <2>
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
Authentication是spring security包中的接口蚊俺,直接繼承自Principal類懈涛,而Principal是位于java.security
包中的∮锯可以見得批钠,Authentication在spring security中是最高級別的身份/認(rèn)證的抽象宇植。
由這個頂級接口,我們可以得到用戶擁有的權(quán)限信息列表埋心,密碼指郁,用戶細(xì)節(jié)信息,用戶身份信息拷呆,認(rèn)證信息闲坎。
上面有提到,authentication.getPrincipal()返回了一個Object茬斧,我們將Principal強(qiáng)轉(zhuǎn)成了Spring Security中最常用的UserDetails腰懂,這在Spring Security中非常常見,接口返回Object项秉,使用instanceof判斷類型绣溜,強(qiáng)轉(zhuǎn)成對應(yīng)的具體實現(xiàn)類。接口詳細(xì)解讀如下:
- getAuthorities()娄蔼,權(quán)限信息列表怖喻,默認(rèn)是GrantedAuthority接口的一些實現(xiàn)類,通常是代表權(quán)限信息的一系列字符串贷屎。
- getCredentials()罢防,密碼信息,用戶輸入的密碼字符串唉侄,在認(rèn)證過后通常會被移除咒吐,用于保障安全。
- getDetails()属划,細(xì)節(jié)信息恬叹,web應(yīng)用中的實現(xiàn)接口通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值同眯。
- getPrincipal()绽昼,敲黑板!P胛稀硅确!最重要的身份信息,大部分情況下返回的是UserDetails接口的實現(xiàn)類明肮,也是框架中的常用接口之一菱农。UserDetails接口將會在下面的小節(jié)重點介紹。
Spring Security是如何完成身份認(rèn)證的柿估?
1 用戶名和密碼被過濾器獲取到循未,封裝成Authentication
,通常情況下是UsernamePasswordAuthenticationToken
這個實現(xiàn)類。
2 AuthenticationManager
身份管理器負(fù)責(zé)驗證這個Authentication
秫舌。
3 認(rèn)證成功后的妖,AuthenticationManager
身份管理器返回一個被填充滿了信息的(包括上面提到的權(quán)限信息绣檬,身份信息,細(xì)節(jié)信息嫂粟,但密碼通常會被移除)Authentication
實例娇未。
4 SecurityContextHolder
安全上下文容器將第3步填充了信息的Authentication
,通過SecurityContextHolder.getContext().setAuthentication(…)方法赋元,設(shè)置到其中忘蟹。
這是一個抽象的認(rèn)證流程,而整個過程中搁凸,如果不糾結(jié)于細(xì)節(jié)媚值,其實只剩下一個AuthenticationManager
是我們沒有接觸過的了,這個身份管理器我們在后面的小節(jié)介紹护糖。將上述的流程轉(zhuǎn)換成代碼褥芒,便是如下的流程:
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
注意:上述這段代碼只是為了讓大家了解Spring Security的工作流程而寫的嫡良,不是什么源碼锰扶。在實際使用中,整個流程會變得更加的復(fù)雜寝受,但是基本思想坷牛,和上述代碼如出一轍。
AuthenticationManager
初次接觸Spring Security的朋友相信會被AuthenticationManager
很澄,ProviderManager
京闰,AuthenticationProvider
…這么多相似的Spring認(rèn)證類搞得暈頭轉(zhuǎn)向,但只要稍微梳理一下就可以理解清楚它們的聯(lián)系和設(shè)計者的用意甩苛。AuthenticationManager(接口)是認(rèn)證相關(guān)的核心接口蹂楣,也是發(fā)起認(rèn)證的出發(fā)點,因為在實際需求中讯蒲,我們可能會允許用戶使用用戶名+密碼登錄痊土,同時允許用戶使用郵箱+密碼,手機(jī)號碼+密碼登錄墨林,甚至赁酝,可能允許用戶使用指紋登錄(還有這樣的操作?沒想到吧)旭等,所以說AuthenticationManager一般不直接認(rèn)證赞哗,AuthenticationManager接口的常用實現(xiàn)類ProviderManager
內(nèi)部會維護(hù)一個List<AuthenticationProvider>
列表,存放多種認(rèn)證方式辆雾,實際上這是委托者模式的應(yīng)用(Delegate)。也就是說月劈,核心的認(rèn)證入口始終只有一個:AuthenticationManager度迂,不同的認(rèn)證方式:用戶名+密碼(UsernamePasswordAuthenticationToken)藤乙,郵箱+密碼,手機(jī)號碼+密碼登錄則對應(yīng)了三個AuthenticationProvider惭墓。這樣一來四不四就好理解多了坛梁?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默認(rèn)策略下腊凶,只需要通過一個AuthenticationProvider的認(rèn)證划咐,即可被認(rèn)為是登錄成功。
只保留了關(guān)鍵認(rèn)證部分的ProviderManager源碼:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 維護(hù)一個AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次認(rèn)證
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息钧萍,則直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密碼
((CredentialsContainer) result).eraseCredentials();
}
//發(fā)布登錄成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//執(zhí)行到此褐缠,說明沒有認(rèn)證成功,包裝異常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager
中的List<authenticationprovider>风瘦,會依照次序去認(rèn)證队魏,認(rèn)證成功則立即返回,若認(rèn)證失敗則返回null万搔,下一個AuthenticationProvider會繼續(xù)嘗試認(rèn)證胡桨,如果所有認(rèn)證器都無法認(rèn)證成功,則ProviderManager
會拋出一個ProviderNotFoundException異常瞬雹。
到這里昧谊,如果不糾結(jié)于AuthenticationProvider的實現(xiàn)細(xì)節(jié)以及安全相關(guān)的過濾器,認(rèn)證相關(guān)的核心類其實都已經(jīng)介紹完畢了:身份信息的存放容器SecurityContextHolder酗捌,身份信息的抽象Authentication呢诬,身份認(rèn)證器AuthenticationManager及其認(rèn)證流程。姑且在這里做一個分隔線意敛。下面來介紹下AuthenticationProvider接口的具體實現(xiàn)馅巷。
DaoAuthenticationProvider
AuthenticationProvider最最最常用的一個實現(xiàn)便是DaoAuthenticationProvider。顧名思義草姻,Dao正是數(shù)據(jù)訪問層的縮寫钓猬,也暗示了這個身份認(rèn)證器的實現(xiàn)思路。由于本文是一個Overview撩独,姑且只給出其UML類圖:
image.png
按照我們最直觀的思路敞曹,怎么去認(rèn)證一個用戶呢?用戶前臺提交了用戶名和密碼综膀,而數(shù)據(jù)庫中保存了用戶名和密碼澳迫,認(rèn)證便是負(fù)責(zé)比對同一個用戶名,提交的密碼和保存的密碼是否相同便是了剧劝。在Spring Security中橄登。提交的用戶名和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據(jù)用戶名加載用戶的任務(wù)則是交給了UserDetailsService拢锹,在DaoAuthenticationProvider中谣妻,對應(yīng)的方法便是retrieveUser,雖然有兩個參數(shù)卒稳,但是retrieveUser只有第一個參數(shù)起主要作用蹋半,返回一個UserDetails。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對充坑,這便是交給additionalAuthenticationChecks方法完成的减江,如果這個void方法沒有拋異常,則認(rèn)為比對成功捻爷。比對密碼的過程辈灼,用到了PasswordEncoder和SaltSource,密碼加密和鹽的概念相信不用我贅述了役衡,它們?yōu)楸U习踩O(shè)計茵休,都是比較基礎(chǔ)的概念。
如果你已經(jīng)被這些概念搞得暈頭轉(zhuǎn)向了手蝎,不妨這么理解DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼榕莺,比對其正確性,如果正確棵介,返回一個數(shù)據(jù)庫中的用戶信息(假設(shè)用戶信息被保存在數(shù)據(jù)庫中)钉鸯。
UserDetails與UserDetailsService
上面不斷提到了UserDetails這個接口,它代表了最詳細(xì)的用戶信息邮辽,這個接口涵蓋了一些必要的用戶信息字段唠雕,具體的實現(xiàn)類對它進(jìn)行了擴(kuò)展。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很類似吨述,比如它們都擁有username岩睁,authorities,區(qū)分他們也是本文的重點內(nèi)容之一揣云。Authentication的getCredentials()與UserDetails中的getPassword()需要被區(qū)分對待捕儒,前者是用戶提交的密碼憑證,后者是用戶正確的密碼邓夕,認(rèn)證器其實就是對這兩者的比對刘莹。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication接口中的getUserDetails()方法嗎焚刚?其中的UserDetails用戶詳細(xì)信息便是經(jīng)過了AuthenticationProvider之后被填充的点弯。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService和AuthenticationProvider兩者的職責(zé)常常被人們搞混,關(guān)于他們的問題在文檔的FAQ和issues中屢見不鮮矿咕。記住一點即可抢肛,敲黑板@桥ァ!捡絮!UserDetailsService只負(fù)責(zé)從特定的地方(通常是數(shù)據(jù)庫)加載用戶信息燃领,僅此而已,記住這一點锦援,可以避免走很多彎路。UserDetailsService常見的實現(xiàn)類有JdbcDaoImpl剥悟,InMemoryUserDetailsManager灵寺,前者從數(shù)據(jù)庫加載用戶,后者從內(nèi)存中加載用戶区岗,也可以自己實現(xiàn)UserDetailsService略板,通常這更加靈活。
架構(gòu)概覽圖
為了更加形象的理解上述我介紹的這些核心類慈缔,網(wǎng)上找到的一張圖
一些Spring Security的過濾器還未囊括在架構(gòu)概覽中叮称,如將表單信息包裝成UsernamePasswordAuthenticationToken的過濾器,考慮到這些雖然也是架構(gòu)的一部分藐鹤,但是真正重寫他們的可能性較小瓤檐,所以打算放到后面的章節(jié)講解。
案例
用戶登陸時會被AuthenticationProcessingFilter攔截娱节,調(diào)用AuthenticationManager的實現(xiàn)挠蛉,而且AuthenticationManager會調(diào)用ProviderManager來獲取用戶驗證信息(不同的Provider調(diào)用的服務(wù)不同,因為這些信息可以是在數(shù)據(jù)庫上肄满,可以是在LDAP服務(wù)器上谴古,可以是xml配置文件上等),如果驗證通過后會將用戶的權(quán)限信息封裝一個UserDetails放到spring的全局緩存SecurityContextHolder中稠歉,以備后面訪問資源時使用掰担。
訪問資源(即授權(quán)管理)訪問url時,會通過AbstractSecurityInterceptor攔截器攔截怒炸,其中會調(diào)用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權(quán)限带饱,在調(diào)用授權(quán)管理器AccessDecisionManager,這個授權(quán)管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權(quán)限信息横媚,還會獲取被攔截的url和被攔截url所需的全部權(quán)限纠炮,然后根據(jù)所配的策略(有:一票決定,一票否定灯蝴,少數(shù)服從多數(shù)等)恢口,如果權(quán)限足夠,則返回穷躁,權(quán)限不夠則報錯并調(diào)用權(quán)限不足頁面耕肩。
配置
有關(guān)于配置文件因妇,在文章的開頭已經(jīng)給出,這里就不過多介紹了猿诸,下面對一些重點內(nèi)容介紹一下
// 表示/resources/**婚被、/lib/**、/timeout梳虽、/verifiCode 不需要過濾
<http security="none" pattern="/resources/**"/>
<http security="none" pattern="/lib/**"/>
<http security="none" pattern="/timeout"/>
<http security="none" pattern="/verifiCode"/>
form-login 表示表單認(rèn)證方式址芯,其實spring security默認(rèn)采用的時 http-basic 認(rèn)證方式(彈窗),當(dāng)我們同時定義了 http-basic 和 form-login 元素時窜觉,form-login 將具有更高的優(yōu)先級谷炸。即在需要認(rèn)證的時候 Spring Security 將引導(dǎo)我們到登錄頁面,而不是彈出一個窗口禀挫。使用form-login認(rèn)證時旬陡,當(dāng)我們什么屬性都不指定的時候 Spring Security 會為我們生成一個默認(rèn)的登錄頁面。如果不想使用默認(rèn)的登錄頁面语婴,我們可以指定自己的登錄頁面描孟。
authentication-success-handler-ref="successHandler" 表示驗證成功時會調(diào)用 CustomAuthenticationSuccessHandler的onAuthenticationSuccess方法,authentication-failure-handler-ref="loginFailureHandler" 表示驗證失敗時會調(diào)用LoginFailureHandler的onAuthenticationFailure方法砰左。
<form-login login-page='/login' authentication-success-handler-ref="successHandler" authentication-failure-handler-ref="loginFailureHandler"/>
流程
在界面點擊登錄的時候匿醒,被AuthenticationProcessingFilter攔截,依次調(diào)用AuthenticationManager中的ProviderManager列表進(jìn)行驗證菜职,ProviderManager的authenticate方法內(nèi)會調(diào)用UserDetailsService的loadUserByUsername方法從數(shù)據(jù)庫或其它地方獲取用戶信息青抛,customUserDetailsService 即是一個UserDetailsService接口的一個實現(xiàn),可以看看它的loadUserByUsername方法
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder ref="passwordManager"/>
</authentication-provider>
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.selectByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("User not found:" + username);
}
checkUserException(user);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
for(String role:user.getRoleCode()){
authorities.add(new SimpleGrantedAuthority(role));
}
UserDetails userDetails = new CustomUserDetails(user.getUserId(), user.getUserName(),
user.getPasswordEncrypted(), true, true, true, true, authorities,user.getEmployeeId(),user.getEmployeeCode());
return userDetails;
}
private void checkUserException(User user) {
UserException ue = null;
if (User.STATUS_LOCK.equalsIgnoreCase(user.getStatus())) {
ue = new UserException(UserException.ERROR_USER_LOCKED, null);
} else if (User.STATUS_EXPR.equalsIgnoreCase(user.getStatus())) {
ue = new UserException(UserException.ERROR_USER_EXPIRED, null);
} else if (user.getStartActiveDate() != null
&& user.getStartActiveDate().getTime() > System.currentTimeMillis()) {
ue = new UserException(UserException.ERROR_USER_NOT_ACTIVE, null);
} else if (user.getEndActiveDate() != null && user.getEndActiveDate().getTime() < System.currentTimeMillis()) {
ue = new UserException(UserException.ERROR_USER_EXPIRED, null);
}
if (ue != null) {
throw new RuntimeException(ue);
}
}
}
<password-encoder ref="passwordManager"/>
表示用戶密碼的加密方式酬核,這里使用的是一個自定義Been蜜另,其源碼如下:
public class PasswordManager implements PasswordEncoder, InitializingBean, SystemConfigListener {
public static final String PASSWORD_COMPLEXITY_NO_LIMIT = "NO_LIMIT";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_LETTERS = "DIGITS_AND_LETTERS";
public static final String PASSWORD_COMPLEXITY_DIGITS_AND_CASE_LETTERS = "DIGITS_AND_CASE_LETTERS";
private PasswordEncoder delegate;
private String siteWideSecret = "my-secret-key";
private String defaultPassword = "123456";
/**
* 密碼失效時間 默認(rèn)0 不失效
*/
private Integer passwordInvalidTime = 0;
/**
* 密碼長度
*/
private Integer passwordMinLength = 8;
/**
* 密碼復(fù)雜度
*/
private String passwordComplexity = "no_limit";
@Override
public void afterPropertiesSet() throws Exception {
delegate = new StandardPasswordEncoder(siteWideSecret);
}
@Override
public String encode(CharSequence rawPassword) {
return delegate.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (StringUtil.isEmpty(encodedPassword)) {
return false;
}
return delegate.matches(rawPassword, encodedPassword);
}
@Override
public List<String> getAcceptedProfiles() {
return Arrays.asList("DEFAULT_PASSWORD", "PASSWORD_INVALID_TIME", "PASSWORD_MIN_LENGTH", "PASSWORD_COMPLEXITY");
}
@Override
public void updateProfile(String profileName, String profileValue) {
if ("PASSWORD_INVALID_TIME".equalsIgnoreCase(profileName)) {
this.passwordInvalidTime = Integer.parseInt(profileValue);
} else if ("PASSWORD_MIN_LENGTH".equalsIgnoreCase(profileName)) {
this.passwordMinLength = Integer.parseInt(profileValue);
} else if ("PASSWORD_COMPLEXITY".equalsIgnoreCase(profileName)) {
this.passwordComplexity = profileValue;
} else if ("DEFAULT_PASSWORD".equalsIgnoreCase(profileName)) {
this.defaultPassword = profileValue;
}
}
}
有關(guān)于這一塊的內(nèi)容,我有一篇專門的文章來介紹 密碼安全
還需要注意的一點嫡意,這里有個登錄前置攔截器:
<!-- 驗證碼攔截器 -->
<custom-filter ref="captchaVerifierFilter" before="FORM_LOGIN_FILTER"/>
spring security中已經(jīng)默認(rèn)有一套攔截器鏈举瑰,但有時并不能完全滿足項目上的需求。默認(rèn)的攔截器如下:
定義 custom-filter 時需要我們通過 ref 屬性指定其對應(yīng)關(guān)聯(lián)的是哪個 Filter蔬螟,此外還需要通過 position此迅、before 或者 after 指定該 Filter 放置的位置。從上圖中可以知道旧巾,F(xiàn)ORM_LOGIN_FILTER 對應(yīng)的就是 UsernamePasswordAuthenticationFilter耸序,before="FORM_LOGIN_FILTER" 就表示將定義的 Filter 放在 FORM_LOGIN_FILTER 之前,也就是將captchaVerifierFilter(驗證碼攔截器)放在UsernamePasswordAuthenticationFilter之前鲁猩。有關(guān)于 CaptchaVerifierFilter 的源碼坎怪,也可以簡單看看,其實就是判斷驗證碼是否正確廓握,不正確就帶上錯誤信息跳轉(zhuǎn)到登錄界面搅窿,這里不再深入嘁酿。
public class CaptchaVerifierFilter extends OncePerRequestFilter {
@Autowired
private ICaptchaManager captchaManager;
@Autowired
private CaptchaConfig captchaConfig;
private RequestMatcher loginRequestMatcher;
private String captchaField = "captcha";
private String loginUrl = "/login";
public CaptchaVerifierFilter() {
setFilterProcessesUrl(this.loginUrl);
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (captchaConfig.isEnableCaptcha(WebUtils.getCookie(httpServletRequest, CaptchaConfig.LOGIN_KEY))
&& requiresValidateCaptcha(httpServletRequest, httpServletResponse)) {
Cookie cookie = WebUtils.getCookie(httpServletRequest, captchaManager.getCaptchaKeyName());
String captchaCode = httpServletRequest.getParameter(getCaptchaField());
if (cookie == null || StringUtils.isEmpty(captchaCode)
|| !captchaManager.checkCaptcha(cookie.getValue(), captchaCode)) {
httpServletRequest.setAttribute("error", true);
httpServletRequest.setAttribute("code", "CAPTCHA_INVALID");
httpServletRequest.setAttribute("exception",
new UserException(UserException.LOGIN_VERIFICATION_CODE_ERROR, null));
httpServletRequest.getRequestDispatcher(loginUrl).forward(httpServletRequest, httpServletResponse);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
public String getCaptchaField() {
return captchaField;
}
public void setCaptchaField(String captchaField) {
this.captchaField = captchaField;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.loginRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
}
protected boolean requiresValidateCaptcha(HttpServletRequest request, HttpServletResponse response) {
return loginRequestMatcher.matches(request) && "POST".equalsIgnoreCase(request.getMethod());
}
}
下面梳理一下流程:用戶點擊登錄按鈕-->xxx攔截-->校驗驗證碼-->xxx處理-->調(diào)用customUserDetailsService的loadUserByUsername方法獲取UserDetails并存起來-->身份認(rèn)證-->根據(jù)認(rèn)證結(jié)果執(zhí)行不同的程序(認(rèn)證成功對應(yīng)authentication-success-handler-ref,認(rèn)證失敗對應(yīng)authentication-failure-handler-ref)男应。
onAuthenticationSuccess
源碼如下
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler implements SystemConfigListener{
@Autowired
private ApplicationContext applicationContext;
private RequestCache requestCache = new HttpSessionRequestCache();
private Logger logger = LoggerFactory.getLogger(getClass());
private Map<String, IAuthenticationSuccessListener> listeners;
public static final String DEFAULT_TARGET_URL = "DEFAULT_TARGET_URL";
private final String loginOauthUrl = "/login?oauth";
private final String loginUrl = "/login";
private final String indexUrl = "/index";
private final String refererStr = "Referer";
private final String loginCasUrl = "/login/cas";
private final String functionCodeStr = "functionCode";
{
setDefaultTargetUrl("/");
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if(listeners == null) {
listeners = applicationContext
.getBeansOfType(IAuthenticationSuccessListener.class);
}
String referer = request.getHeader(refererStr);
if(referer != null && referer.endsWith(loginOauthUrl)){
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
List<IAuthenticationSuccessListener> list = new ArrayList<>();
list.addAll(listeners.values());
Collections.sort(list);
IAuthenticationSuccessListener successListener = null;
try {
for (IAuthenticationSuccessListener listener : list) {
successListener = listener;
successListener.onAuthenticationSuccess(request, response, authentication);
}
HttpSession session = request.getSession(false);
session.setAttribute(User.LOGIN_CHANGE_INDEX,"CHANGE");
} catch (Exception e) {
logger.error("authentication success, but error occurred in " + successListener, e);
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
request.setAttribute("error", true);
request.setAttribute("exception", e);
request.getRequestDispatcher("/login").forward(request, response);
return;
}
String requestURI = request.getRequestURI();
boolean isCas = requestURI.endsWith(loginCasUrl);
if(isCas) {
//拿到登錄以前的url
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
/* String defaultTarget = getDefaultTargetUrl();
if (!targetUrl.contains(functionCodeStr) && ! indexUrl.equalsIgnoreCase(defaultTarget)) {
targetUrl = getDefaultTargetUrl()+"?targetUrl="+targetUrl;
}*/
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
return;
}
}
handle(request, response, authentication);
}
@Override
public List<String> getAcceptedProfiles() {
return Arrays.asList(DEFAULT_TARGET_URL);
}
@Override
public void updateProfile(String profileName, String profileValue) {
if(StringUtil.isNotEmpty(profileValue)) {
setDefaultTargetUrl(profileValue);
}
}
}
注意這個 private Map<String, IAuthenticationSuccessListener> listeners
得到的就是的IAuthenticationSuccessListener接口的實現(xiàn)類
主要有:GenerateTokenAuthenticationSuccessListener闹司、DefaultAuthenticationSuccessListener、AuthenticationSuccessActivityListener沐飘、UserLoginInfoCollection游桩。然后遍歷listeners,調(diào)用各個listener的onAuthenticationSuccess方法耐朴。
GenerateTokenAuthenticationSuccessListener:保存 token 授權(quán)信息到redis中
DefaultAuthenticationSuccessListener:設(shè)置系統(tǒng)首頁的一些配置
AuthenticationSuccessActivityListener:activity代辦事項
UserLoginInfoCollection:用戶登錄記錄
LoginFailureHandler
驗證失敗時的處理比較簡單众弓,主要就是返回錯誤碼,然后跳轉(zhuǎn)到登錄界面隔箍。
public class LoginFailureHandler implements AuthenticationFailureHandler {
private static final Logger log = LoggerFactory.getLogger(LoginFailureHandler.class);
@Autowired
private CaptchaConfig captchaConfig;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (log.isDebugEnabled()) {
log.debug("login failed");
}
if (captchaConfig.getWrongTimes() > 0) {
captchaConfig.updateLoginFailureInfo(WebUtils.getCookie(request, CaptchaConfig.LOGIN_KEY));
}
request.setAttribute("error", true);
request.setAttribute("code", "LOGIN_NOT_MATCH");
request.setAttribute("exception", exception);
request.getRequestDispatcher("/login").forward(request, response);
}
}
大體的邏輯就是這樣,但是中間還有太多太多細(xì)節(jié)脚乡,想要完全理解清除需要很大的精力蜒滩,其實如果知道這個流程,就基本上可以根據(jù)項目上的需求去修改代碼了奶稠。
基于投票的AccessDecisionManager實現(xiàn)
AccessDecisionManager
- 提供了一個基于AccessDecisionVoter 接口和投票集合的授權(quán)機(jī)制俯艰。
- supports:這個邏輯操作實際上包含兩個方法,它們允許AccessDecisionManager 的實現(xiàn)
類判斷是否支持當(dāng)前的請求锌订。 - decide:基于請求的上下文和安全配置竹握,允許AccessDecisionManager 去核實訪問是否被
允許以及請求是否能夠被接受。decide 方法實際上沒有返回值辆飘,通過拋出異常來表明對
請求訪問的拒絕啦辐。
AbstractAccessDecisionManager implements AccessDecisionManager。AbstractAccessDecisionManager 實現(xiàn)類如下:
spring security文檔
以下截圖是文檔的一部分
There are three concrete AccessDecisionManager s provided with Spring Security that tally the votes. The ConsensusBased implementation will grant or deny access based on the consensus of non-abstain votes. Properties are provided to control behavior in the event of an equality of votes or if all votes are abstain. The AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were received (i.e. a deny vote will be ignored, provided there was at least one grant vote). Like the ConsensusBased implementation, there is a parameter that controls the behavior if all voters abstain. The UnanimousBased provider expects unanimous ACCESS_GRANTED votes in order to grant access, ignoring abstains. It will deny access if there is any ACCESS_DENIED vote. Like the other implementations, there is a parameter that controls the behaviour if all voters abstain.
It is possible to implement a custom AccessDecisionManager that tallies votes differently. For example, votes from a particular AccessDecisionVoter might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect.
在Hap的標(biāo)準(zhǔn)登錄中蜈项,使用的是UnanimousBased投票機(jī)制芹关,意識就是說需要所有的投票器都認(rèn)證通過才算驗證通過。
<!-- 認(rèn)證管理器,確定用戶,角色及相應(yīng)的權(quán)限 -->
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<!-- 投票器 -->
<beans:constructor-arg>
<beans:list>
<beans:bean class="com.hand.hap.security.CustomWebExpressionVoter"/>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
<beans:bean class="com.hand.hap.security.PermissionVoter"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
追溯到UnanimousBased源碼紧卒,查看對應(yīng)的UML類圖:
UnanimousBased類中只有一個方法侥衬,其實現(xiàn)如下
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) throws AccessDeniedException {
int grant = 0;
int abstain = 0;
List<ConfigAttribute> singleAttributeList = new ArrayList<ConfigAttribute>(1);
singleAttributeList.add(null);
for (ConfigAttribute attribute : attributes) {
singleAttributeList.set(0, attribute);
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, singleAttributeList);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
grant++;
break;
case AccessDecisionVoter.ACCESS_DENIED:
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied",
"Access is denied"));
default:
abstain++;
break;
}
}
}
// To get this far, there were no deny votes
if (grant > 0) {
return;
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
其實就是在循環(huán)遍歷配置文件中的那幾個 實現(xiàn)類的 vote 方法跑芳。
再通過IDEA中的DEBUG功能轴总,發(fā)現(xiàn)是在 org.springframework.security.access.intercept.AbstractSecurityInterceptor 的 beforeInvocation 方法中調(diào)用了 UnanimousBased 的 decide 方法漂佩;
org.springframework.security.web.access.intercept.FilterSecurityInterceptor 中調(diào)用了beforeInvocation方法