Authentication 是一個接口,用來表示用戶認(rèn)證信息的,在用戶登錄認(rèn)證之前相關(guān)信息會封裝為一個 Authentication 具體實現(xiàn)類的對象,在登錄認(rèn)證成功之后又會生成一個信息更全面,包含用戶權(quán)限等信息的 Authentication 對象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后續(xù)的程序進(jìn)行調(diào)用,如訪問權(quán)限的鑒定等。
SecurityContextHolder 是用來保存 SecurityContext 的。SecurityContext 中含有當(dāng)前正在訪問系統(tǒng)的用戶的詳細(xì)信息。默認(rèn)情況下,SecurityContextHolder 將使用 ThreadLocal 來保存 SecurityContext,這也就意味著在處于同一線程中的方法中我們可以從 ThreadLocal 中獲取到當(dāng)前的 SecurityContext。因為線程池的原因,如果我們每次在請求完成后都將 ThreadLocal 進(jìn)行清除的話,那么我們把 SecurityContext 存放在 ThreadLocal 中還是比較安全的。這些工作 Spring Security 已經(jīng)自動為我們做了,即在每一次 request 結(jié)束后都將清除當(dāng)前線程的 ThreadLocal。
SecurityContextHolder 中定義了一系列的靜態(tài)方法,而這些靜態(tài)方法內(nèi)部邏輯基本上都是通過 SecurityContextHolder 持有的 SecurityContextHolderStrategy 來實現(xiàn)的,如 getContext()、setContext()、clearContext()等。而默認(rèn)使用的 strategy 就是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy。另外,Spring Security 還提供了兩種類型的 strategy 實現(xiàn),GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一個 SecurityContext,如 C/S 結(jié)構(gòu)的客戶端;后者使用 InheritableThreadLocal 來存放 SecurityContext,即子線程可以使用父線程中存放的變量。
一般而言,我們使用默認(rèn)的 strategy 就可以了,但是如果要改變默認(rèn)的 strategy,Spring Security 為我們提供了兩種方法,這兩種方式都是通過改變 strategyName 來實現(xiàn)的。SecurityContextHolder 中為三種不同類型的 strategy 分別命名為 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL。第一種方式是通過 SecurityContextHolder 的靜態(tài)方法 setStrategyName() 來指定需要使用的 strategy;第二種方式是通過系統(tǒng)屬性進(jìn)行指定,其中屬性名默認(rèn)為 “spring.security.strategy”,屬性值為對應(yīng) strategy 的名稱。
Spring Security 使用一個 Authentication 對象來描述當(dāng)前用戶的相關(guān)信息。SecurityContextHolder 中持有的是當(dāng)前用戶的 SecurityContext,而 SecurityContext 持有的是代表當(dāng)前用戶相關(guān)信息的 Authentication 的引用。這個 Authentication 對象不需要我們自己去創(chuàng)建,在與系統(tǒng)交互的過程中,Spring Security 會自動為我們創(chuàng)建相應(yīng)的 Authentication 對象,然后賦值給當(dāng)前的 SecurityContext。但是往往我們需要在程序中獲取當(dāng)前用戶的相關(guān)信息,比如最常見的是獲取當(dāng)前登錄用戶的用戶名。在程序的任何地方,通過如下方式我們可以獲取到當(dāng)前用戶的用戶名。
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() 可以獲取到代表當(dāng)前用戶的信息,這個對象通常是 UserDetails 的實例。獲取當(dāng)前用戶的用戶名是一種比較常見的需求,關(guān)于上述代碼其實 Spring Security 在 Authentication 中的實現(xiàn)類中已經(jīng)為我們做了相關(guān)實現(xiàn),所以獲取當(dāng)前用戶的用戶名最簡單的方式應(yīng)當(dāng)如下。
public String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
此外,調(diào)用 SecurityContextHolder.getContext() 獲取 SecurityContext 時,如果對應(yīng)的 SecurityContext 不存在,則 Spring Security 將為我們建立一個空的 SecurityContext 并進(jìn)行返回。
AuthenticationManager 是一個用來處理認(rèn)證(Authentication)請求的接口。在其中只定義了一個方法 authenticate(),該方法只接收一個代表認(rèn)證請求的 Authentication 對象作為參數(shù),如果認(rèn)證成功,則會返回一個封裝了當(dāng)前用戶權(quán)限等信息的 Authentication 對象進(jìn)行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在 Spring Security 中,AuthenticationManager 的默認(rèn)實現(xiàn)是 ProviderManager,而且它不直接自己處理認(rèn)證請求,而是委托給其所配置的 AuthenticationProvider 列表,然后會依次使用每一個 AuthenticationProvider 進(jìn)行認(rèn)證,如果有一個 AuthenticationProvider 認(rèn)證后的結(jié)果不為 null,則表示該 AuthenticationProvider 已經(jīng)認(rèn)證成功,之后的 AuthenticationProvider 將不再繼續(xù)認(rèn)證。然后直接以該 AuthenticationProvider 的認(rèn)證結(jié)果作為 ProviderManager 的認(rèn)證結(jié)果。如果所有的 AuthenticationProvider 的認(rèn)證結(jié)果都為 null,則表示認(rèn)證失敗,將拋出一個 ProviderNotFoundException。校驗認(rèn)證請求最常用的方法是根據(jù)請求的用戶名加載對應(yīng)的 UserDetails,然后比對 UserDetails 的密碼與認(rèn)證請求的密碼是否一致,一致則表示認(rèn)證通過。Spring Security 內(nèi)部的 DaoAuthenticationProvider 就是使用的這種方式。其內(nèi)部使用 UserDetailsService 來負(fù)責(zé)加載 UserDetails,UserDetailsService 將在下節(jié)講解。在認(rèn)證成功以后會使用加載的 UserDetails 來封裝要返回的 Authentication 對象,加載的 UserDetails 對象是包含用戶權(quán)限等信息的。認(rèn)證成功返回的 Authentication 對象將會保存在當(dāng)前的 SecurityContext 中。
當(dāng)我們在使用 NameSpace 時, authentication-manager 元素的使用會使 Spring Security 在內(nèi)部創(chuàng)建一個 ProviderManager,然后可以通過 authentication-provider 元素往其中添加 AuthenticationProvider。當(dāng)定義 authentication-provider 元素時,如果沒有通過 ref 屬性指定關(guān)聯(lián)哪個 AuthenticationProvider,Spring Security 默認(rèn)就會使用 DaoAuthenticationProvider。使用了 NameSpace 后我們就不要再聲明 ProviderManager 了。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
user-service-ref="userDetailsService"/>
</security:authentication-manager>
如果我們沒有使用 NameSpace,那么我們就應(yīng)該在 ApplicationContext 中聲明一個 ProviderManager。
默認(rèn)情況下,在認(rèn)證成功后 ProviderManager 將清除返回的 Authentication 中的憑證信息,如密碼。所以如果你在無狀態(tài)的應(yīng)用中將返回的 Authentication 信息緩存起來了,那么以后你再利用緩存的信息去認(rèn)證將會失敗,因為它已經(jīng)不存在密碼這樣的憑證信息了。所以在使用緩存的時候你應(yīng)該考慮到這個問題。一種解決辦法是設(shè)置 ProviderManager 的 eraseCredentialsAfterAuthentication 屬性為 false,或者想辦法在緩存時將憑證信息一起緩存。
通過 Authentication.getPrincipal() 的返回類型是 Object,但很多情況下其返回的其實是一個 UserDetails 的實例。UserDetails 是 Spring Security 中一個核心的接口。其中定義了一些可以獲取用戶名、密碼、權(quán)限等與認(rèn)證相關(guān)的信息的方法。Spring Security 內(nèi)部使用的 UserDetails 實現(xiàn)類大都是內(nèi)置的 User 類,我們?nèi)绻褂?UserDetails 時也可以直接使用該類。在 Spring Security 內(nèi)部很多地方需要使用用戶信息的時候基本上都是使用的 UserDetails,比如在登錄認(rèn)證的時候。登錄認(rèn)證的時候 Spring Security 會通過 UserDetailsService 的 loadUserByUsername() 方法獲取對應(yīng)的 UserDetails 進(jìn)行認(rèn)證,認(rèn)證通過后會將該 UserDetails 賦給認(rèn)證通過的 Authentication 的 principal,然后再把該 Authentication 存入到 SecurityContext 中。之后如果需要使用用戶信息的時候就是通過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。
通常我們需要在應(yīng)用中獲取當(dāng)前用戶的其它信息,如 Email、電話等。這時存放在 Authentication 的 principal 中只包含有認(rèn)證相關(guān)信息的 UserDetails 對象可能就不能滿足我們的要求了。這時我們可以實現(xiàn)自己的 UserDetails,在該實現(xiàn)類中我們可以定義一些獲取用戶其它信息的方法,這樣將來我們就可以直接從當(dāng)前 SecurityContext 的 Authentication 的 principal 中獲取這些信息了。上文已經(jīng)提到了 UserDetails 是通過 UserDetailsService 的 loadUserByUsername() 方法進(jìn)行加載的。UserDetailsService 也是一個接口,我們也需要實現(xiàn)自己的 UserDetailsService 來加載我們自定義的 UserDetails 信息。然后把它指定給 AuthenticationProvider 即可。如下是一個配置 UserDetailsService 的示例。
<!-- 用于認(rèn)證的 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 的實現(xiàn),另外 Spring Security 還為我們提供了 UserDetailsService 另外一個實現(xiàn),InMemoryDaoImpl。
其作用是從數(shù)據(jù)庫中加載 UserDetails 信息。其中已經(jīng)定義好了加載相關(guān)信息的默認(rèn)腳本,這些腳本也可以通過 JdbcDaoImpl 的相關(guān)屬性進(jìn)行指定。關(guān)于 JdbcDaoImpl 使用方式會在講解 AuthenticationProvider 的時候做一個相對詳細(xì)一點(diǎn)的介紹。
JdbcDaoImpl 允許我們從數(shù)據(jù)庫來加載 UserDetails,其底層使用的是 Spring 的 JdbcTemplate 進(jìn)行操作,所以我們需要給其指定一個數(shù)據(jù)源。此外,我們需要通過 usersByUsernameQuery 屬性指定通過 username 查詢用戶信息的 SQL 語句;通過 authoritiesByUsernameQuery 屬性指定通過 username 查詢用戶所擁有的權(quán)限的 SQL 語句;如果我們通過設(shè)置 JdbcDaoImpl 的 enableGroups 為 true 啟用了用戶組權(quán)限的支持,則我們還需要通過 groupAuthoritiesByUsernameQuery 屬性指定根據(jù) username 查詢用戶組權(quán)限的 SQL 語句。當(dāng)這些信息都沒有指定時,將使用默認(rèn)的 SQL 語句,默認(rèn)的 SQL 語句如下所示。
select username, password, enabled from users where username=? -- 根據(jù) username 查詢用戶信息
select username, authority from authorities where username=? -- 根據(jù) username 查詢用戶權(quán)限信息
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 -- 根據(jù) username 查詢用戶組權(quán)限
使用默認(rèn)的 SQL 語句進(jìn)行查詢時意味著我們對應(yīng)的數(shù)據(jù)庫中應(yīng)該有對應(yīng)的表和表結(jié)構(gòu),Spring Security 為我們提供的默認(rèn)表的創(chuàng)建腳本如下。
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 默認(rèn)使用的就是 JdbcDaoImpl。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider>
<!-- 基于 Jdbc 的 UserDetailsService 實現(xiàn),JdbcDaoImpl -->
<security:jdbc-user-service data-source-ref="dataSource"/>
</security:authentication-provider>
</security:authentication-manager>
InMemoryDaoImpl 主要是測試用的,其只是簡單的將用戶信息保存在內(nèi)存中。使用 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,其對應(yīng)的密碼為 user,擁有 ROLE_USER 的權(quán)限。此外,user-service 還支持通過 properties 文件來指定用戶信息,如:
<security:user-service properties="/WEB-INF/config/users.properties"/>
其中屬性文件應(yīng)遵循如下格式:
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
所以,對應(yīng)上面的配置文件,我們的 users.properties 文件的內(nèi)容應(yīng)該如下所示:
#username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
user=user,ROLE_USER
Authentication 的 getAuthorities() 可以返回當(dāng)前 Authentication 對象擁有的權(quán)限,即當(dāng)前用戶擁有的權(quán)限。其返回值是一個 GrantedAuthority 類型的數(shù)組,每一個 GrantedAuthority 對象代表賦予給當(dāng)前用戶的一種權(quán)限。GrantedAuthority 是一個接口,其通常是通過 UserDetailsService 進(jìn)行加載,然后賦予給 UserDetails 的。
GrantedAuthority 中只定義了一個 getAuthority() 方法,該方法返回一個字符串,表示對應(yīng)權(quán)限的字符串表示,如果對應(yīng)權(quán)限不能用字符串表示,則應(yīng)當(dāng)返回 null。
Spring Security 針對 GrantedAuthority 有一個簡單實現(xiàn) SimpleGrantedAuthority。該類只是簡單的接收一個表示權(quán)限的字符串。Spring Security 內(nèi)部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 來封裝 Authentication 對象。
更多建議: