Authentication 是一個接口,用來表示用戶認證信息的,在用戶登陸認證以前相關信息會封裝爲一個 Authentication 具體實現類的對象,在登陸認證成功以後又會生成一個信息更全面,包含用戶權限等信息的 Authentication 對象,而後把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供後續的程序進行調用,如訪問權限的鑑定等。spring
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 中的實現類中已經爲咱們作了相關實現,因此獲取當前用戶的用戶名最簡單的方式應當以下。ide
public String getCurrentUsername() { return SecurityContextHolder.getContext().getAuthentication().getName(); }
此外,調用 SecurityContextHolder.getContext() 獲取 SecurityContext 時,若是對應的 SecurityContext 不存在,則 Spring Security 將爲咱們創建一個空的 SecurityContext 並進行返回。測試
AuthenticationManager 是一個用來處理認證(Authentication)請求的接口。在其中只定義了一個方法 authenticate(),該方法只接收一個表明認證請求的 Authentication 對象做爲參數,若是認證成功,則會返回一個封裝了當前用戶權限等信息的 Authentication 對象進行返回。spa
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在 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 了。code
<security:authentication-manager alias="authenticationManager"> <security:authentication-provider user-service-ref="userDetailsService"/> </security:authentication-manager>
若是咱們沒有使用 NameSpace,那麼咱們就應該在 ApplicationContext 中聲明一個 ProviderManager。
默認狀況下,在認證成功後 ProviderManager 將清除返回的 Authentication 中的憑證信息,如密碼。因此若是你在無狀態的應用中將返回的 Authentication 信息緩存起來了,那麼之後你再利用緩存的信息去認證將會失敗,由於它已經不存在密碼這樣的憑證信息了。因此在使用緩存的時候你應該考慮到這個問題。一種解決辦法是設置 ProviderManager 的 eraseCredentialsAfterAuthentication 屬性爲 false,或者想辦法在緩存時將憑證信息一塊兒緩存。
經過 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 的時候作一個相對詳細一點的介紹。
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>
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 類型的數組,每個 GrantedAuthority 對象表明賦予給當前用戶的一種權限。GrantedAuthority 是一個接口,其一般是經過 UserDetailsService 進行加載,而後賦予給 UserDetails 的。
GrantedAuthority 中只定義了一個 getAuthority() 方法,該方法返回一個字符串,表示對應權限的字符串表示,若是對應權限不能用字符串表示,則應當返回 null。
Spring Security 針對 GrantedAuthority 有一個簡單實現 SimpleGrantedAuthority。該類只是簡單的接收一個表示權限的字符串。Spring Security 內部的全部 AuthenticationProvider 都是使用 SimpleGrantedAuthority 來封裝 Authentication 對象。