鍍金池/ 教程/ Java/ 核心類簡(jiǎn)介
初體驗(yàn)
權(quán)限鑒定基礎(chǔ)
Remember-Me 功能
匿名認(rèn)證
intercept-url配置
認(rèn)證簡(jiǎn)介
退出登錄 logout
AuthenticationProvider
Filter
關(guān)于登錄
異常信息本地化
緩存 UserDetails
session 管理
核心類簡(jiǎn)介

核心類簡(jiǎn)介

Authentication

Authentication 是一個(gè)接口,用來表示用戶認(rèn)證信息的,在用戶登錄認(rèn)證之前相關(guān)信息會(huì)封裝為一個(gè) Authentication 具體實(shí)現(xiàn)類的對(duì)象,在登錄認(rèn)證成功之后又會(huì)生成一個(gè)信息更全面,包含用戶權(quán)限等信息的 Authentication 對(duì)象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后續(xù)的程序進(jìn)行調(diào)用,如訪問權(quán)限的鑒定等。

SecurityContextHolder

SecurityContextHolder 是用來保存 SecurityContext 的。SecurityContext 中含有當(dāng)前正在訪問系統(tǒng)的用戶的詳細(xì)信息。默認(rèn)情況下,SecurityContextHolder 將使用 ThreadLocal 來保存 SecurityContext,這也就意味著在處于同一線程中的方法中我們可以從 ThreadLocal 中獲取到當(dāng)前的 SecurityContext。因?yàn)榫€程池的原因,如果我們每次在請(qǐng)求完成后都將 ThreadLocal 進(jìn)行清除的話,那么我們把 SecurityContext 存放在 ThreadLocal 中還是比較安全的。這些工作 Spring Security 已經(jīng)自動(dòng)為我們做了,即在每一次 request 結(jié)束后都將清除當(dāng)前線程的 ThreadLocal。

SecurityContextHolder 中定義了一系列的靜態(tài)方法,而這些靜態(tài)方法內(nèi)部邏輯基本上都是通過 SecurityContextHolder 持有的 SecurityContextHolderStrategy 來實(shí)現(xiàn)的,如 getContext()、setContext()、clearContext()等。而默認(rèn)使用的 strategy 就是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy。另外,Spring Security 還提供了兩種類型的 strategy 實(shí)現(xiàn),GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一個(gè) SecurityContext,如 C/S 結(jié)構(gòu)的客戶端;后者使用 InheritableThreadLocal 來存放 SecurityContext,即子線程可以使用父線程中存放的變量。

一般而言,我們使用默認(rèn)的 strategy 就可以了,但是如果要改變默認(rèn)的 strategy,Spring Security 為我們提供了兩種方法,這兩種方式都是通過改變 strategyName 來實(shí)現(xiàn)的。SecurityContextHolder 中為三種不同類型的 strategy 分別命名為 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL。第一種方式是通過 SecurityContextHolder 的靜態(tài)方法 setStrategyName() 來指定需要使用的 strategy;第二種方式是通過系統(tǒng)屬性進(jìn)行指定,其中屬性名默認(rèn)為 “spring.security.strategy”,屬性值為對(duì)應(yīng) strategy 的名稱。

Spring Security 使用一個(gè) Authentication 對(duì)象來描述當(dāng)前用戶的相關(guān)信息。SecurityContextHolder 中持有的是當(dāng)前用戶的 SecurityContext,而 SecurityContext 持有的是代表當(dāng)前用戶相關(guān)信息的 Authentication 的引用。這個(gè) Authentication 對(duì)象不需要我們自己去創(chuàng)建,在與系統(tǒng)交互的過程中,Spring Security 會(huì)自動(dòng)為我們創(chuàng)建相應(yīng)的 Authentication 對(duì)象,然后賦值給當(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)前用戶的信息,這個(gè)對(duì)象通常是 UserDetails 的實(shí)例。獲取當(dāng)前用戶的用戶名是一種比較常見的需求,關(guān)于上述代碼其實(shí) Spring Security 在 Authentication 中的實(shí)現(xiàn)類中已經(jīng)為我們做了相關(guān)實(shí)現(xiàn),所以獲取當(dāng)前用戶的用戶名最簡(jiǎn)單的方式應(yīng)當(dāng)如下。

   public String getCurrentUsername() {
      return SecurityContextHolder.getContext().getAuthentication().getName();
   }

此外,調(diào)用 SecurityContextHolder.getContext() 獲取 SecurityContext 時(shí),如果對(duì)應(yīng)的 SecurityContext 不存在,則 Spring Security 將為我們建立一個(gè)空的 SecurityContext 并進(jìn)行返回。

AuthenticationManager 和 AuthenticationProvider

AuthenticationManager 是一個(gè)用來處理認(rèn)證(Authentication)請(qǐng)求的接口。在其中只定義了一個(gè)方法 authenticate(),該方法只接收一個(gè)代表認(rèn)證請(qǐng)求的 Authentication 對(duì)象作為參數(shù),如果認(rèn)證成功,則會(huì)返回一個(gè)封裝了當(dāng)前用戶權(quán)限等信息的 Authentication 對(duì)象進(jìn)行返回。

    Authentication authenticate(Authentication authentication) throws AuthenticationException;

在 Spring Security 中,AuthenticationManager 的默認(rèn)實(shí)現(xiàn)是 ProviderManager,而且它不直接自己處理認(rèn)證請(qǐng)求,而是委托給其所配置的 AuthenticationProvider 列表,然后會(huì)依次使用每一個(gè) AuthenticationProvider 進(jìn)行認(rèn)證,如果有一個(gè) 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)證失敗,將拋出一個(gè) ProviderNotFoundException。校驗(yàn)認(rèn)證請(qǐng)求最常用的方法是根據(jù)請(qǐng)求的用戶名加載對(duì)應(yīng)的 UserDetails,然后比對(duì) UserDetails 的密碼與認(rèn)證請(qǐng)求的密碼是否一致,一致則表示認(rèn)證通過。Spring Security 內(nèi)部的 DaoAuthenticationProvider 就是使用的這種方式。其內(nèi)部使用 UserDetailsService 來負(fù)責(zé)加載 UserDetails,UserDetailsService 將在下節(jié)講解。在認(rèn)證成功以后會(huì)使用加載的 UserDetails 來封裝要返回的 Authentication 對(duì)象,加載的 UserDetails 對(duì)象是包含用戶權(quán)限等信息的。認(rèn)證成功返回的 Authentication 對(duì)象將會(huì)保存在當(dāng)前的 SecurityContext 中。

當(dāng)我們?cè)谑褂?NameSpace 時(shí), authentication-manager 元素的使用會(huì)使 Spring Security 在內(nèi)部創(chuàng)建一個(gè) ProviderManager,然后可以通過 authentication-provider 元素往其中添加 AuthenticationProvider。當(dāng)定義 authentication-provider 元素時(shí),如果沒有通過 ref 屬性指定關(guān)聯(lián)哪個(gè) AuthenticationProvider,Spring Security 默認(rèn)就會(huì)使用 DaoAuthenticationProvider。使用了 NameSpace 后我們就不要再聲明 ProviderManager 了。

   <security:authentication-manager alias="authenticationManager">
      <security:authentication-provider
         user-service-ref="userDetailsService"/>
   </security:authentication-manager>

如果我們沒有使用 NameSpace,那么我們就應(yīng)該在 ApplicationContext 中聲明一個(gè) ProviderManager。

認(rèn)證成功后清除憑證

默認(rèn)情況下,在認(rèn)證成功后 ProviderManager 將清除返回的 Authentication 中的憑證信息,如密碼。所以如果你在無狀態(tài)的應(yīng)用中將返回的 Authentication 信息緩存起來了,那么以后你再利用緩存的信息去認(rèn)證將會(huì)失敗,因?yàn)樗呀?jīng)不存在密碼這樣的憑證信息了。所以在使用緩存的時(shí)候你應(yīng)該考慮到這個(gè)問題。一種解決辦法是設(shè)置 ProviderManager 的 eraseCredentialsAfterAuthentication 屬性為 false,或者想辦法在緩存時(shí)將憑證信息一起緩存。

UserDetailsService

通過 Authentication.getPrincipal() 的返回類型是 Object,但很多情況下其返回的其實(shí)是一個(gè) UserDetails 的實(shí)例。UserDetails 是 Spring Security 中一個(gè)核心的接口。其中定義了一些可以獲取用戶名、密碼、權(quán)限等與認(rèn)證相關(guān)的信息的方法。Spring Security 內(nèi)部使用的 UserDetails 實(shí)現(xiàn)類大都是內(nèi)置的 User 類,我們?nèi)绻褂?UserDetails 時(shí)也可以直接使用該類。在 Spring Security 內(nèi)部很多地方需要使用用戶信息的時(shí)候基本上都是使用的 UserDetails,比如在登錄認(rèn)證的時(shí)候。登錄認(rèn)證的時(shí)候 Spring Security 會(huì)通過 UserDetailsService 的 loadUserByUsername() 方法獲取對(duì)應(yīng)的 UserDetails 進(jìn)行認(rèn)證,認(rèn)證通過后會(huì)將該 UserDetails 賦給認(rèn)證通過的 Authentication 的 principal,然后再把該 Authentication 存入到 SecurityContext 中。之后如果需要使用用戶信息的時(shí)候就是通過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。

通常我們需要在應(yīng)用中獲取當(dāng)前用戶的其它信息,如 Email、電話等。這時(shí)存放在 Authentication 的 principal 中只包含有認(rèn)證相關(guān)信息的 UserDetails 對(duì)象可能就不能滿足我們的要求了。這時(shí)我們可以實(shí)現(xiàn)自己的 UserDetails,在該實(shí)現(xiàn)類中我們可以定義一些獲取用戶其它信息的方法,這樣將來我們就可以直接從當(dāng)前 SecurityContext 的 Authentication 的 principal 中獲取這些信息了。上文已經(jīng)提到了 UserDetails 是通過 UserDetailsService 的 loadUserByUsername() 方法進(jìn)行加載的。UserDetailsService 也是一個(gè)接口,我們也需要實(shí)現(xiàn)自己的 UserDetailsService 來加載我們自定義的 UserDetails 信息。然后把它指定給 AuthenticationProvider 即可。如下是一個(gè)配置 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 的實(shí)現(xiàn),另外 Spring Security 還為我們提供了 UserDetailsService 另外一個(gè)實(shí)現(xiàn),InMemoryDaoImpl。

其作用是從數(shù)據(jù)庫中加載 UserDetails 信息。其中已經(jīng)定義好了加載相關(guān)信息的默認(rèn)腳本,這些腳本也可以通過 JdbcDaoImpl 的相關(guān)屬性進(jìn)行指定。關(guān)于 JdbcDaoImpl 使用方式會(huì)在講解 AuthenticationProvider 的時(shí)候做一個(gè)相對(duì)詳細(xì)一點(diǎn)的介紹。

JdbcDaoImpl

JdbcDaoImpl 允許我們從數(shù)據(jù)庫來加載 UserDetails,其底層使用的是 Spring 的 JdbcTemplate 進(jìn)行操作,所以我們需要給其指定一個(gè)數(shù)據(jù)源。此外,我們需要通過 usersByUsernameQuery 屬性指定通過 username 查詢用戶信息的 SQL 語句;通過 authoritiesByUsernameQuery 屬性指定通過 username 查詢用戶所擁有的權(quán)限的 SQL 語句;如果我們通過設(shè)置 JdbcDaoImpl 的 enableGroups 為 true 啟用了用戶組權(quán)限的支持,則我們還需要通過 groupAuthoritiesByUsernameQuery 屬性指定根據(jù) username 查詢用戶組權(quán)限的 SQL 語句。當(dāng)這些信息都沒有指定時(shí),將使用默認(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)行查詢時(shí)意味著我們對(duì)應(yīng)的數(shù)據(jù)庫中應(yīng)該有對(duì)應(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 元素時(shí)在底層 Spring Security 默認(rèn)使用的就是 JdbcDaoImpl。

   <security:authentication-manager alias="authenticationManager">
      <security:authentication-provider>
         <!-- 基于 Jdbc 的 UserDetailsService 實(shí)現(xiàn),JdbcDaoImpl -->
         <security:jdbc-user-service data-source-ref="dataSource"/>
      </security:authentication-provider>
   </security:authentication-manager>

InMemoryDaoImpl

InMemoryDaoImpl 主要是測(cè)試用的,其只是簡(jiǎn)單的將用戶信息保存在內(nèi)存中。使用 NameSpace 時(shí),使用 user-service 元素 Spring Security 底層使用的 UserDetailsService 就是 InMemoryDaoImpl。此時(shí),我們可以簡(jiǎn)單的使用 user 元素來定義一個(gè) UserDetails。

   <security:user-service>
      <security:user name="user" password="user" authorities="ROLE_USER"/>
   </security:user-service>

如上配置表示我們定義了一個(gè)用戶 user,其對(duì)應(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]

所以,對(duì)應(yīng)上面的配置文件,我們的 users.properties 文件的內(nèi)容應(yīng)該如下所示:

#username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
user=user,ROLE_USER

GrantedAuthority

Authentication 的 getAuthorities() 可以返回當(dāng)前 Authentication 對(duì)象擁有的權(quán)限,即當(dāng)前用戶擁有的權(quán)限。其返回值是一個(gè) GrantedAuthority 類型的數(shù)組,每一個(gè) GrantedAuthority 對(duì)象代表賦予給當(dāng)前用戶的一種權(quán)限。GrantedAuthority 是一個(gè)接口,其通常是通過 UserDetailsService 進(jìn)行加載,然后賦予給 UserDetails 的。

GrantedAuthority 中只定義了一個(gè) getAuthority() 方法,該方法返回一個(gè)字符串,表示對(duì)應(yīng)權(quán)限的字符串表示,如果對(duì)應(yīng)權(quán)限不能用字符串表示,則應(yīng)當(dāng)返回 null。

Spring Security 針對(duì) GrantedAuthority 有一個(gè)簡(jiǎn)單實(shí)現(xiàn) SimpleGrantedAuthority。該類只是簡(jiǎn)單的接收一個(gè)表示權(quán)限的字符串。Spring Security 內(nèi)部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 來封裝 Authentication 對(duì)象。

上一篇:Remember-Me 功能下一篇:session 管理