目錄
1.1 Authentication
1.2 SecurityContextHolder
1.3 AuthenticationManager和AuthenticationProvider
1.3.1 認證成功后清除憑證
1.4 UserDetailsService
1.4.1 JdbcDaoImpl
1.4.2 InMemoryDaoImpl
1.5 GrantedAuthority
1.1 Authentication
Authentication是一個接口,用來表示用戶認證信息的,在用戶登錄認證之前相關信息會封裝為一個Authentication具體實現類的對象,在登錄認證成功之后又會生成一個信息更全面,包含用戶權限等信息的Authentication對象,然后把它保存在SecurityContextHolder所持有的SecurityContext中,供后續的程序進行調用,如訪問權限的鑒定等。
1.2 SecurityContextHolder
SecurityContextHolder是用來保存SecurityContext的。SecurityContext中含有當前正在訪問系統的用戶的詳細信息。默認情況下,SecurityContextHolder將使用ThreadLocal來保存SecurityContext,這也就意味着在處於同一線程中的方法中我們可以從ThreadLocal中獲取到當前的SecurityContext。因為線程池的原因,如果我們每次在請求完成后都將ThreadLocal進行清除的話,那么我們把SecurityContext存放在ThreadLocal中還是比較安全的。這些工作Spring Security已經自動為我們做了,即在每一次request結束后都將清除當前線程的ThreadLocal。
SecurityContextHolder中定義了一系列的靜態方法,而這些靜態方法內部邏輯基本上都是通過SecurityContextHolder持有的SecurityContextHolderStrategy來實現的,如getContext()、setContext()、clearContext()等。而默認使用的strategy就是基於ThreadLocal的ThreadLocalSecurityContextHolderStrategy。另外,Spring Security還提供了兩種類型的strategy實現,GlobalSecurityContextHolderStrategy和InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一個SecurityContext,如C/S結構的客戶端;后者使用InheritableThreadLocal來存放SecurityContext,即子線程可以使用父線程中存放的變量。
一般而言,我們使用默認的strategy就可以了,但是如果要改變默認的strategy,Spring Security為我們提供了兩種方法,這兩種方式都是通過改變strategyName來實現的。SecurityContextHolder中為三種不同類型的strategy分別命名為MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL和MODE_GLOBAL。第一種方式是通過SecurityContextHolder的靜態方法setStrategyName()來指定需要使用的strategy;第二種方式是通過系統屬性進行指定,其中屬性名默認為“spring.security.strategy”,屬性值為對應strategy的名稱。
Spring Security使用一個Authentication對象來描述當前用戶的相關信息。SecurityContextHolder中持有的是當前用戶的SecurityContext,而SecurityContext持有的是代表當前用戶相關信息的Authentication的引用。這個Authentication對象不需要我們自己去創建,在與系統交互的過程中,Spring Security會自動為我們創建相應的Authentication對象,然后賦值給當前的SecurityContext。但是往往我們需要在程序中獲取當前用戶的相關信息,比如最常見的是獲取當前登錄用戶的用戶名。在程序的任何地方,通過如下方式我們可以獲取到當前用戶的用戶名。
public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return String.valueOf(principal);
}
通過Authentication.getPrincipal()可以獲取到代表當前用戶的信息,這個對象通常是UserDetails的實例。獲取當前用戶的用戶名是一種比較常見的需求,關於上述代碼其實Spring Security在Authentication中的實現類中已經為我們做了相關實現,所以獲取當前用戶的用戶名最簡單的方式應當如下。
public String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
此外,調用SecurityContextHolder.getContext()獲取SecurityContext時,如果對應的SecurityContext不存在,則Spring Security將為我們建立一個空的SecurityContext並進行返回。
1.3 AuthenticationManager和AuthenticationProvider
AuthenticationManager是一個用來處理認證(Authentication)請求的接口。在其中只定義了一個方法authenticate(),該方法只接收一個代表認證請求的Authentication對象作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等信息的Authentication對象進行返回。
Authentication authenticate(Authentication authentication) throwsAuthenticationException;
在Spring Security中,AuthenticationManager的默認實現是ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的AuthenticationProvider列表,然后會依次使用每一個AuthenticationProvider進行認證,如果有一個AuthenticationProvider認證后的結果不為null,則表示該AuthenticationProvider已經認證成功,之后的AuthenticationProvider將不再繼續認證。然后直接以該AuthenticationProvider的認證結果作為ProviderManager的認證結果。如果所有的AuthenticationProvider的認證結果都為null,則表示認證失敗,將拋出一個ProviderNotFoundException。校驗認證請求最常用的方法是根據請求的用戶名加載對應的UserDetails,然后比對UserDetails的密碼與認證請求的密碼是否一致,一致則表示認證通過。Spring Security內部的DaoAuthenticationProvider就是使用的這種方式。其內部使用UserDetailsService來負責加載UserDetails,UserDetailsService將在下節講解。在認證成功以后會使用加載的UserDetails來封裝要返回的Authentication對象,加載的UserDetails對象是包含用戶權限等信息的。認證成功返回的Authentication對象將會保存在當前的SecurityContext中。
當我們在使用NameSpace時, authentication-manager元素的使用會使Spring Security 在內部創建一個ProviderManager,然后可以通過authentication-provider元素往其中添加AuthenticationProvider。當定義authentication-provider元素時,如果沒有通過ref屬性指定關聯哪個AuthenticationProvider,Spring Security默認就會使用DaoAuthenticationProvider。使用了NameSpace后我們就不要再聲明ProviderManager了。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
user-service-ref="userDetailsService"/>
</security:authentication-manager>
如果我們沒有使用NameSpace,那么我們就應該在ApplicationContext中聲明一個ProviderManager。
1.3.1 認證成功后清除憑證
默認情況下,在認證成功后ProviderManager將清除返回的Authentication中的憑證信息,如密碼。所以如果你在無狀態的應用中將返回的Authentication信息緩存起來了,那么以后你再利用緩存的信息去認證將會失敗,因為它已經不存在密碼這樣的憑證信息了。所以在使用緩存的時候你應該考慮到這個問題。一種解決辦法是設置ProviderManager的eraseCredentialsAfterAuthentication 屬性為false,或者想辦法在緩存時將憑證信息一起緩存。
1.4 UserDetailsService
通過Authentication.getPrincipal()的返回類型是Object,但很多情況下其返回的其實是一個UserDetails的實例。UserDetails是Spring Security中一個核心的接口。其中定義了一些可以獲取用戶名、密碼、權限等與認證相關的信息的方法。Spring Security內部使用的UserDetails實現類大都是內置的User類,我們如果要使用UserDetails時也可以直接使用該類。在Spring Security內部很多地方需要使用用戶信息的時候基本上都是使用的UserDetails,比如在登錄認證的時候。登錄認證的時候Spring Security會通過UserDetailsService的loadUserByUsername()方法獲取對應的UserDetails進行認證,認證通過后會將該UserDetails賦給認證通過的Authentication的principal,然后再把該Authentication存入到SecurityContext中。之后如果需要使用用戶信息的時候就是通過SecurityContextHolder獲取存放在SecurityContext中的Authentication的principal。
通常我們需要在應用中獲取當前用戶的其它信息,如Email、電話等。這時存放在Authentication的principal中只包含有認證相關信息的UserDetails對象可能就不能滿足我們的要求了。這時我們可以實現自己的UserDetails,在該實現類中我們可以定義一些獲取用戶其它信息的方法,這樣將來我們就可以直接從當前SecurityContext的Authentication的principal中獲取這些信息了。上文已經提到了UserDetails是通過UserDetailsService的loadUserByUsername()方法進行加載的。UserDetailsService也是一個接口,我們也需要實現自己的UserDetailsService來加載我們自定義的UserDetails信息。然后把它指定給AuthenticationProvider即可。如下是一個配置UserDetailsService的示例。
<!-- 用於認證的AuthenticationManager -->
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
user-service-ref="userDetailsService" />
</security:authentication-manager>
<bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
上述代碼中我們使用的JdbcDaoImpl是Spring Security為我們提供的UserDetailsService的實現,另外Spring Security還為我們提供了UserDetailsService另外一個實現,InMemoryDaoImpl。
其作用是從數據庫中加載UserDetails信息。其中已經定義好了加載相關信息的默認腳本,這些腳本也可以通過JdbcDaoImpl的相關屬性進行指定。關於JdbcDaoImpl使用方式會在講解AuthenticationProvider的時候做一個相對詳細一點的介紹。
1.4.1 JdbcDaoImpl
JdbcDaoImpl允許我們從數據庫來加載UserDetails,其底層使用的是Spring的JdbcTemplate進行操作,所以我們需要給其指定一個數據源。此外,我們需要通過usersByUsernameQuery屬性指定通過username查詢用戶信息的SQL語句;通過authoritiesByUsernameQuery屬性指定通過username查詢用戶所擁有的權限的SQL語句;如果我們通過設置JdbcDaoImpl的enableGroups為true啟用了用戶組權限的支持,則我們還需要通過groupAuthoritiesByUsernameQuery屬性指定根據username查詢用戶組權限的SQL語句。當這些信息都沒有指定時,將使用默認的SQL語句,默認的SQL語句如下所示。
select username, password, enabled from users where username=? --根據username查詢用戶信息
select username, authority from authorities where username=? --根據username查詢用戶權限信息
select g.id, g.group_name, ga.authority from groups g, groups_members gm, groups_authorities ga where gm.username=? and g.id=ga.group_id and g.id=gm.group_id --根據username查詢用戶組權限
使用默認的SQL語句進行查詢時意味着我們對應的數據庫中應該有對應的表和表結構,Spring Security為我們提供的默認表的創建腳本如下。
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) notnull);
create table group_authorities (
group_id bigint notnull,
authority varchar(50) notnull,
constraint fk_group_authorities_group foreign key(group_id) references groups(id));
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) notnull,
group_id bigint notnull,
constraint fk_group_members_group foreign key(group_id) references groups(id));
此外,使用jdbc-user-service元素時在底層Spring Security默認使用的就是JdbcDaoImpl。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider>
<!-- 基於Jdbc的UserDetailsService實現,JdbcDaoImpl -->
<security:jdbc-user-service data-source-ref="dataSource"/>
</security:authentication-provider>
</security:authentication-manager>
1.4.2 InMemoryDaoImpl
InMemoryDaoImpl主要是測試用的,其只是簡單的將用戶信息保存在內存中。使用NameSpace時,使用user-service元素Spring Security底層使用的UserDetailsService就是InMemoryDaoImpl。此時,我們可以簡單的使用user元素來定義一個UserDetails。
<security:user-service>
<security:user name="user" password="user" authorities="ROLE_USER"/>
</security:user-service>
如上配置表示我們定義了一個用戶user,其對應的密碼為user,擁有ROLE_USER的權限。此外,user-service還支持通過properties文件來指定用戶信息,如:
<security:user-service properties="/WEB-INF/config/users.properties"/>
其中屬性文件應遵循如下格式:
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
所以,對應上面的配置文件,我們的users.properties文件的內容應該如下所示:
#username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
user=user,ROLE_USER
1.5 GrantedAuthority
Authentication的getAuthorities()可以返回當前Authentication對象擁有的權限,即當前用戶擁有的權限。其返回值是一個GrantedAuthority類型的數組,每一個GrantedAuthority對象代表賦予給當前用戶的一種權限。GrantedAuthority是一個接口,其通常是通過UserDetailsService進行加載,然后賦予給UserDetails的。
GrantedAuthority中只定義了一個getAuthority()方法,該方法返回一個字符串,表示對應權限的字符串表示,如果對應權限不能用字符串表示,則應當返回null。
Spring Security針對GrantedAuthority有一個簡單實現SimpleGrantedAuthority。該類只是簡單的接收一個表示權限的字符串。Spring Security內部的所有AuthenticationProvider都是使用SimpleGrantedAuthority來封裝Authentication對象。
(注:本文是基於Spring Security3.1.6所寫)