Spring Security - 核心類(lèi)簡(jiǎn)介 - 獲得登錄用戶的相關(guān)信息(轉(zhuǎn))

核心類(lèi)簡(jiǎn)介

目錄

  1. Authentication
  2. SecurityContextHolder
  3. AuthenticationManager和AuthenticationProvider
    3.1 認(rèn)證成功后清除憑證
  4. UserDetailsService
    4.1 JdbcDaoImpl
    4.2 InMemoryDaoImpl
  5. GrantedAuthority

1. Authentication

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

2. SecurityContextHolder

SecurityContextHolder是用來(lái)保存SecurityContext的。SecurityContext中含有當(dāng)前正在訪問(wèn)系統(tǒng)的用戶的詳細(xì)信息。默認(rèn)情況下,SecurityContextHolder將使用ThreadLocal來(lái)保存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)部邏輯基本上都是通過(guò)SecurityContextHolder持有的SecurityContextHolderStrategy來(lái)實(shí)現(xiàn)的,如getContext()、setContext()、clearContext()等。而默認(rèn)使用的strategy就是基于ThreadLocal的ThreadLocalSecurityContextHolderStrategy。另外,Spring Security還提供了兩種類(lèi)型的strategy實(shí)現(xiàn),GlobalSecurityContextHolderStrategy和InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一個(gè)SecurityContext,如C/S結(jié)構(gòu)的客戶端;后者使用InheritableThreadLocal來(lái)存放SecurityContext,即子線程可以使用父線程中存放的變量。

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

Spring Security使用一個(gè)Authentication對(duì)象來(lái)描述當(dāng)前用戶的相關(guān)信息。SecurityContextHolder中持有的是當(dāng)前用戶的SecurityContext,而SecurityContext持有的是代表當(dāng)前用戶相關(guān)信息的Authentication的引用。這個(gè)Authentication對(duì)象不需要我們自己去創(chuàng)建,在與系統(tǒng)交互的過(guò)程中,Spring Security會(huì)自動(dòng)為我們創(chuàng)建相應(yīng)的Authentication對(duì)象,然后賦值給當(dāng)前的SecurityContext。但是往往我們需要在程序中獲取當(dāng)前用戶的相關(guān)信息,比如最常見(jiàn)的是獲取當(dāng)前登錄用戶的用戶名。在程序的任何地方,通過(guò)如下方式我們可以獲取到當(dāng)前用戶的用戶名。

publicString getCurrentUsername() {
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if(principalinstanceofUserDetails) {
        return((UserDetails) principal).getUsername();
    }
    if(principalinstanceofPrincipal) {
        return((Principal) principal).getName();
    }
    returnString.valueOf(principal);
}

通過(guò)Authentication.getPrincipal()可以獲取到代表當(dāng)前用戶的信息,這個(gè)對(duì)象通常是UserDetails的實(shí)例。獲取當(dāng)前用戶的用戶名是一種比較常見(jiàn)的需求,關(guān)于上述代碼其實(shí)Spring Security在Authentication中的實(shí)現(xiàn)類(lèi)中已經(jīng)為我們做了相關(guān)實(shí)現(xiàn),所以獲取當(dāng)前用戶的用戶名最簡(jiǎn)單的方式應(yīng)當(dāng)如下。

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

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

3. AuthenticationManager和AuthenticationProvider

AuthenticationManager是一個(gè)用來(lái)處理認(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)行返回。

public 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)證通過(guò)。Spring Security內(nèi)部的DaoAuthenticationProvider就是使用的這種方式。其內(nèi)部使用UserDetailsService來(lái)負(fù)責(zé)加載UserDetails,UserDetailsService將在下節(jié)講解。在認(rèn)證成功以后會(huì)使用加載的UserDetails來(lái)封裝要返回的Authentication對(duì)象,加載的UserDetails對(duì)象是包含用戶權(quán)限等信息的。認(rèn)證成功返回的Authentication對(duì)象將會(huì)保存在當(dāng)前的SecurityContext中。

當(dāng)我們?cè)谑褂肗ameSpace時(shí), authentication-manager元素的使用會(huì)使Spring Security 在內(nèi)部創(chuàng)建一個(gè)ProviderManager,然后可以通過(guò)authentication-provider元素往其中添加AuthenticationProvider。當(dāng)定義authentication-provider元素時(shí),如果沒(méi)有通過(guò)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>

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

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

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

<!-- 認(rèn)證管理器 -->
<authentication-manager alias="authenticationManager" erase-credentials="false">
      <authentication-provider user-service-ref="cachingUserDetailsService" />
</authentication-manager>

<!-- 可以緩存 UserDetails 的 UserDetailsService -->
<beans:bean id="cachingUserDetailsService" class="org.springframework.security.config.authentication.CachingUserDetailsService">
      <!-- 真正加載 UserDetails 的 UserDetailsService -->
      <beans:constructor-arg ref="userDetailsService"/>
      <!-- 緩存 UserDetails 的 UserCache -->
      <beans:property name="userCache">
            <beans:bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache">
                  <!-- 用于真正緩存的 Ehcache 對(duì)象 -->
                  <beans:property name="cache" ref="ehcache4UserDetails"/>
            </beans:bean>
      </beans:property>
</beans:bean>

<!-- 將使用默認(rèn)的 CacheManager 創(chuàng)建一個(gè)名為 ehcache4UserDetails 的 Ehcache 對(duì)象 -->
<beans:bean id="ehcache4UserDetails" class="org.springframework.cache.ehcache.EhCacheFactoryBean"/>

<!-- 從數(shù)據(jù)庫(kù)加載 UserDetails 的 UserDetailsService -->
<beans:bean id="userDetailsService" class="org.spring.security.userdetails.UserDetailsServiceCustom" />

4. UserDetailsService

通過(guò)Authentication.getPrincipal()的返回類(lèi)型是Object,但很多情況下其返回的其實(shí)是一個(gè)UserDetails的實(shí)例。UserDetails是Spring Security中一個(gè)核心的接口。其中定義了一些可以獲取用戶名、密碼、權(quán)限等與認(rèn)證相關(guān)的信息的方法。Spring Security內(nèi)部使用的UserDetails實(shí)現(xiàn)類(lèi)大都是內(nèi)置的User類(lèi),我們?nèi)绻褂肬serDetails時(shí)也可以直接使用該類(lèi)。在Spring Security內(nèi)部很多地方需要使用用戶信息的時(shí)候基本上都是使用的UserDetails,比如在登錄認(rèn)證的時(shí)候。登錄認(rèn)證的時(shí)候Spring Security會(huì)通過(guò)UserDetailsService的loadUserByUsername()方法獲取對(duì)應(yīng)的UserDetails進(jìn)行認(rèn)證,認(rèn)證通過(guò)后會(huì)將該UserDetails賦給認(rèn)證通過(guò)的Authentication的principal,然后再把該Authentication存入到SecurityContext中。之后如果需要使用用戶信息的時(shí)候就是通過(guò)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)類(lèi)中我們可以定義一些獲取用戶其它信息的方法,這樣將來(lái)我們就可以直接從當(dāng)前SecurityContext的Authentication的principal中獲取這些信息了。上文已經(jīng)提到了UserDetails是通過(guò)UserDetailsService的loadUserByUsername()方法進(jìn)行加載的。UserDetailsService也是一個(gè)接口,我們也需要實(shí)現(xiàn)自己的UserDetailsService來(lái)加載我們自定義的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ù)庫(kù)中加載UserDetails信息。其中已經(jīng)定義好了加載相關(guān)信息的默認(rèn)腳本,這些腳本也可以通過(guò)JdbcDaoImpl的相關(guān)屬性進(jìn)行指定。關(guān)于JdbcDaoImpl使用方式會(huì)在講解AuthenticationProvider的時(shí)候做一個(gè)相對(duì)詳細(xì)一點(diǎn)的介紹。

4.1 JdbcDaoImpl

JdbcDaoImpl允許我們從數(shù)據(jù)庫(kù)來(lái)加載UserDetails,其底層使用的是Spring的JdbcTemplate進(jìn)行操作,所以我們需要給其指定一個(gè)數(shù)據(jù)源。此外,我們需要通過(guò)usersByUsernameQuery屬性指定通過(guò)username查詢(xún)用戶信息的SQL語(yǔ)句;通過(guò)authoritiesByUsernameQuery屬性指定通過(guò)username查詢(xún)用戶所擁有的權(quán)限的SQL語(yǔ)句;如果我們通過(guò)設(shè)置JdbcDaoImpl的enableGroups為true啟用了用戶組權(quán)限的支持,則我們還需要通過(guò)groupAuthoritiesByUsernameQuery屬性指定根據(jù)username查詢(xún)用戶組權(quán)限的SQL語(yǔ)句。當(dāng)這些信息都沒(méi)有指定時(shí),將使用默認(rèn)的SQL語(yǔ)句,默認(rèn)的SQL語(yǔ)句如下所示。

-- 根據(jù)username查詢(xún)用戶信息
select username, password, enabled from users where username=?

-- 根據(jù)username查詢(xún)用戶權(quán)限信息
select username, authority from authorities where username=? 

-- 根據(jù)username查詢(xún)用戶組權(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 

使用默認(rèn)的SQL語(yǔ)句進(jìn)行查詢(xún)時(shí)意味著我們對(duì)應(yīng)的數(shù)據(jù)庫(kù)中應(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) referencesusers(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>

4.2 InMemoryDaoImpl

InMemoryDaoImpl主要是測(cè)試用的,其只是簡(jiǎn)單的將用戶信息保存在內(nèi)存中。使用NameSpace時(shí),使用user-service元素Spring Security底層使用的UserDetailsService就是InMemoryDaoImpl。此時(shí),我們可以簡(jiǎn)單的使用user元素來(lái)定義一個(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還支持通過(guò)properties文件來(lái)指定用戶信息,如:

<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

5. GrantedAuthority

Authentication的getAuthorities()可以返回當(dāng)前Authentication對(duì)象擁有的權(quán)限,即當(dāng)前用戶擁有的權(quán)限。其返回值是一個(gè)GrantedAuthority類(lèi)型的數(shù)組,每一個(gè)GrantedAuthority對(duì)象代表賦予給當(dāng)前用戶的一種權(quán)限。GrantedAuthority是一個(gè)接口,其通常是通過(guò)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。該類(lèi)只是簡(jiǎn)單的接收一個(gè)表示權(quán)限的字符串。Spring Security內(nèi)部的所有AuthenticationProvider都是使用SimpleGrantedAuthority來(lái)封裝Authentication對(duì)象。

  • (注:本文是基于Spring Security3.1.6所寫(xiě))

(注:原文地址:http://elim.iteye.com/blog/2155786

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容