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