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

Remember-Me 功能

概述

Remember-Me 是指網(wǎng)站能夠在 Session 之間記住登錄用戶的身份,具體來(lái)說(shuō)就是我成功認(rèn)證一次之后在一定的時(shí)間內(nèi)我可以不用再輸入用戶名和密碼進(jìn)行登錄了,系統(tǒng)會(huì)自動(dòng)給我登錄。這通常是通過(guò)服務(wù)端發(fā)送一個(gè) cookie 給客戶端瀏覽器,下次瀏覽器再訪問(wèn)服務(wù)端時(shí)服務(wù)端能夠自動(dòng)檢測(cè)客戶端的 cookie,根據(jù) cookie 值觸發(fā)自動(dòng)登錄操作。Spring Security 為這些操作的發(fā)生提供必要的鉤子,并且針對(duì)于 Remember-Me 功能有兩種實(shí)現(xiàn)。一種是簡(jiǎn)單的使用加密來(lái)保證基于 cookie 的 token 的安全,另一種是通過(guò)數(shù)據(jù)庫(kù)或其它持久化存儲(chǔ)機(jī)制來(lái)保存生成的 token。

需要注意的是兩種實(shí)現(xiàn)都需要一個(gè) UserDetailsService。如果你使用的 AuthenticationProvider 不使用 UserDetailsService,那么記住我將會(huì)不起作用,除非在你的 ApplicationContext 中擁有一個(gè) UserDetailsService 類型的 bean。

基于簡(jiǎn)單加密 token 的方法

當(dāng)用戶選擇了記住我成功登錄后,Spring Security 將會(huì)生成一個(gè) cookie 發(fā)送給客戶端瀏覽器。cookie 值由如下方式組成:

base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key))

  • username:登錄的用戶名。
  • password:登錄的密碼。
  • expirationTime:token 失效的日期和時(shí)間,以毫秒表示。
  • key:用來(lái)防止修改 token 的一個(gè) key。

這樣用來(lái)實(shí)現(xiàn) Remember-Me 功能的 token 只能在指定的時(shí)間內(nèi)有效,且必須保證 token 中所包含的 username、password 和 key 沒(méi)有被改變才行。需要注意的是,這樣做其實(shí)是存在安全隱患的,那就是在用戶獲取到實(shí)現(xiàn)記住我功能的 token 后,任何用戶都可以在該 token 過(guò)期之前通過(guò)該 token 進(jìn)行自動(dòng)登錄。如果用戶發(fā)現(xiàn)自己的 token 被盜用了,那么他可以通過(guò)改變自己的登錄密碼來(lái)立即使其所有的記住我 token 失效。如果希望我們的應(yīng)用能夠更安全一點(diǎn),可以使用接下來(lái)要介紹的持久化 token 方式,或者不使用 Remember-Me 功能,因?yàn)?Remember-Me 功能總是有點(diǎn)不安全的。

使用這種方式時(shí),我們只需要在 http 元素下定義一個(gè) remember-me 元素,同時(shí)指定其 key 屬性即可。key 屬性是用來(lái)標(biāo)記存放 token 的 cookie 的,對(duì)應(yīng)上文提到的生成 token 時(shí)的那個(gè) key。

   <security:http auto-config="true">
      <security:form-login/>
      <!-- 定義記住我功能 -->
      <security:remember-me key="elim"/>
      <security:intercept-url pattern="/**" access="ROLE_USER" />
   </security:http>

這里有兩個(gè)需要注意的地方。第一,如果你的登錄頁(yè)面是自定義的,那么需要在登錄頁(yè)面上新增一個(gè)名為 “_spring_security_remember_me” 的 checkbox,這是基于 NameSpace 定義提供的默認(rèn)名稱,如果要自定義可以自己定義 TokenBasedRememberMeServices 或 PersistentTokenBasedRememberMeServices 對(duì)應(yīng)的 bean,然后通過(guò)其 parameter 屬性進(jìn)行指定,具體操作請(qǐng)參考后文關(guān)于《Remember-Me 相關(guān)接口和實(shí)現(xiàn)類》部分內(nèi)容。第二,上述功能需要一個(gè) UserDetailsService,如果在你的 ApplicationContext 中已經(jīng)擁有一個(gè)了,那么 Spring Security 將自動(dòng)獲?。蝗绻麤](méi)有,那么當(dāng)然你需要定義一個(gè);如果擁有在 ApplicationContext 中擁有多個(gè) UserDetailsService 定義,那么你需要通過(guò) remember-me 元素的 user-service-ref 屬性指定將要使用的那個(gè)。如:

   <security:http auto-config="true">
      <security:form-login/>
      <!-- 定義記住我功能,通過(guò) user-service-ref 指定將要使用的 UserDetailsService-->
      <security:remember-me key="elim" user-service-ref="userDetailsService"/>
      <security:intercept-url pattern="/**" access="ROLE_USER" />
   </security:http>

   <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
      <property name="dataSource" ref="dataSource"/>
   </bean>

基于持久化 token 的方法

持久化 token 的方法跟簡(jiǎn)單加密 token 的方法在實(shí)現(xiàn) Remember-Me 功能上大體相同,都是在用戶選擇了 “記住我” 成功登錄后,將生成的 token 存入 cookie 中并發(fā)送到客戶端瀏覽器,待到下次用戶訪問(wèn)系統(tǒng)時(shí),系統(tǒng)將直接從客戶端 cookie 中讀取 token 進(jìn)行認(rèn)證。所不同的是基于簡(jiǎn)單加密 token 的方法,一旦用戶登錄成功后,生成的 token 將在客戶端保存一段時(shí)間,如果用戶不點(diǎn)擊退出登錄,或者不修改密碼,那么在 cookie 失效之前,他都可以使用該 token 進(jìn)行登錄,哪怕該 token 被別人盜用了,用戶與盜用者都同樣可以進(jìn)行登錄。而基于持久化 token 的方法采用這樣的實(shí)現(xiàn)邏輯:

  1. 用戶選擇了 “記住我” 成功登錄后,將會(huì)把 username、隨機(jī)產(chǎn)生的序列號(hào)、生成的 token 存入一個(gè)數(shù)據(jù)庫(kù)表中,同時(shí)將它們的組合生成一個(gè) cookie 發(fā)送給客戶端瀏覽器。
  2. 當(dāng)下一次沒(méi)有登錄的用戶訪問(wèn)系統(tǒng)時(shí),首先檢查 cookie,如果對(duì)應(yīng) cookie 中包含的 username、序列號(hào)和 token 與數(shù)據(jù)庫(kù)中保存的一致,則表示其通過(guò)驗(yàn)證,系統(tǒng)將重新生成一個(gè)新的 token 替換數(shù)據(jù)庫(kù)中對(duì)應(yīng)組合的舊 token,序列號(hào)保持不變,同時(shí)刪除舊的 cookie,重新生成包含新生成的 token,就的序列號(hào)和 username 的 cookie 發(fā)送給客戶端。
  3. 如果檢查 cookie 時(shí),cookie 中包含的 username 和序列號(hào)跟數(shù)據(jù)庫(kù)中保存的匹配,但是 token 不匹配。這種情況極有可能是因?yàn)槟愕?cookie 被人盜用了,由于盜用者使用你原本通過(guò)認(rèn)證的 cookie 進(jìn)行登錄了導(dǎo)致舊的 token 失效,而產(chǎn)生了新的 token。這個(gè)時(shí)候 Spring Security 就可以發(fā)現(xiàn) cookie 被盜用的情況,它將刪除數(shù)據(jù)庫(kù)中與當(dāng)前用戶相關(guān)的所有 token 記錄,這樣盜用者使用原有的 cookie 將不能再登錄,同時(shí)提醒用戶其帳號(hào)有被盜用的可能性。
  4. 如果對(duì)應(yīng) cookie 不存在,或者包含的 username 和序列號(hào)與數(shù)據(jù)庫(kù)中保存的不一致,那么將會(huì)引導(dǎo)用戶到登錄頁(yè)面。

從以上邏輯我們可以看出持久化 token 的方法比簡(jiǎn)單加密 token 的方法更安全,因?yàn)橐坏┠愕?cookie 被人盜用了,你只要再利用原有的 cookie 試圖自動(dòng)登錄一次,原有的 token 將失效導(dǎo)致盜用者不能再使用原來(lái)盜用的 cookie 進(jìn)行登錄了,同時(shí)用戶可以發(fā)現(xiàn)自己的 cookie 有被盜用的可能性。但因?yàn)?cookie 被盜用后盜用者還可以在用戶下一次登錄前順利的進(jìn)行登錄,所以如果你的應(yīng)用對(duì)安全性要求比較高就不要使用 Remember-Me 功能了。

使用持久化 token 方法時(shí)需要我們的數(shù)據(jù)庫(kù)中擁有如下表及其表結(jié)構(gòu)。

create table persistent_logins (username varchar(64) not null,
                                    series varchar(64) primary key,
                                    token varchar(64) not null,
                                    last_used timestamp not null)

然后還是通過(guò) remember-me 元素來(lái)使用,只是這個(gè)時(shí)候我們需要其 data-source-ref 屬性指定對(duì)應(yīng)的數(shù)據(jù)源,同時(shí)別忘了它也同樣需要 ApplicationContext 中擁有 UserDetailsService,如果擁有多個(gè),請(qǐng)使用 user-service-ref 屬性指定 remember-me 使用的是哪一個(gè)。

<security:http auto-config="true">
      <security:form-login/>
      <!-- 定義記住我功能 -->
      <security:remember-me data-source-ref="dataSource"/>
      <security:intercept-url pattern="/**" access="ROLE_USER" />
   </security:http>

Remember-Me 相關(guān)接口和實(shí)現(xiàn)類

在上述介紹中,我們實(shí)現(xiàn) Remember-Me 功能是通過(guò) Spring Security 為了簡(jiǎn)化 Remember-Me 而提供的 NameSpace 進(jìn)行定義的。而底層實(shí)際上還是通過(guò) RememberMeServices、UsernamePasswordAuthenticationFilter 和 RememberMeAuthenticationFilter 的協(xié)作來(lái)完成的。RememberMeServices 是 Spring Security 為 Remember-Me 提供的一個(gè)服務(wù)接口,其定義如下。

publicinterface RememberMeServices {
    /**
     * 自動(dòng)登錄。在實(shí)現(xiàn)這個(gè)方法的時(shí)候應(yīng)該判斷用戶提供的 Remember-Me cookie 是否有效,如果無(wú)效,應(yīng)當(dāng)直接忽略。
     * 如果認(rèn)證成功應(yīng)當(dāng)返回一個(gè) AuthenticationToken,推薦返回 RememberMeAuthenticationToken;
     * 如果認(rèn)證不成功應(yīng)當(dāng)返回 null。
     */
    Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
    /**
     * 在用戶登錄失敗時(shí)調(diào)用。實(shí)現(xiàn)者應(yīng)當(dāng)做一些類似于刪除 cookie 之類的處理。
     */
    void loginFail(HttpServletRequest request, HttpServletResponse response);
    /**
     * 在用戶成功登錄后調(diào)用。實(shí)現(xiàn)者可以在這里判斷用戶是否選擇了 “Remember-Me” 登錄,然后做相應(yīng)的處理。
     */
    void loginSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication successfulAuthentication);
}

UsernamePasswordAuthenticationFilter 擁有一個(gè) RememberMeServices 的引用,默認(rèn)是一個(gè)空實(shí)現(xiàn)的 NullRememberMeServices,而實(shí)際當(dāng)我們通過(guò) remember-me 定義啟用 Remember-Me 時(shí),它會(huì)是一個(gè)具體的實(shí)現(xiàn)。用戶的請(qǐng)求會(huì)先通過(guò) UsernamePasswordAuthenticationFilter,如認(rèn)證成功會(huì)調(diào)用 RememberMeServices 的 loginSuccess() 方法,否則調(diào)用 RememberMeServices 的 loginFail() 方法。UsernamePasswordAuthenticationFilter 是不會(huì)調(diào)用 RememberMeServices 的 autoLogin() 方法進(jìn)行自動(dòng)登錄的。之后運(yùn)行到 RememberMeAuthenticationFilter 時(shí)如果檢測(cè)到還沒(méi)有登錄,那么 RememberMeAuthenticationFilter 會(huì)嘗試著調(diào)用所包含的 RememberMeServices 的 autoLogin() 方法進(jìn)行自動(dòng)登錄。關(guān)于 RememberMeServices Spring Security 已經(jīng)為我們提供了兩種實(shí)現(xiàn),分別對(duì)應(yīng)于前文提到的基于簡(jiǎn)單加密 token 和基于持久化 token 的方法。

TokenBasedRememberMeServices

TokenBasedRememberMeServices 對(duì)應(yīng)于前文介紹的使用 namespace 時(shí)基于簡(jiǎn)單加密 token 的實(shí)現(xiàn)。TokenBasedRememberMeServices 會(huì)在用戶選擇了記住我成功登錄后,生成一個(gè)包含 token 信息的 cookie 發(fā)送到客戶端;如果用戶登錄失敗則會(huì)刪除客戶端保存的實(shí)現(xiàn) Remember-Me 的 cookie。需要自動(dòng)登錄時(shí),它會(huì)判斷 cookie 中所包含的關(guān)于 Remember-Me 的信息是否與系統(tǒng)一致,一致則返回一個(gè) RememberMeAuthenticationToken 供 RememberMeAuthenticationProvider 處理,不一致則會(huì)刪除客戶端的 Remember-Me cookie。TokenBasedRememberMeServices 還實(shí)現(xiàn)了 Spring Security 的 LogoutHandler 接口,所以它可以在用戶退出登錄時(shí)立即清除 Remember-Me cookie。

如果把使用 namespace 定義 Remember-Me 改為直接定義 RememberMeServices 和對(duì)應(yīng)的 Filter 來(lái)使用的話,那么我們可以如下定義。

   <security:http>
      <security:form-login login-page="/login.jsp"/>
      <security:intercept-url pattern="/login*.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
      <security:intercept-url pattern="/**" access="ROLE_USER" />
      <!-- 把 usernamePasswordAuthenticationFilter 加入 FilterChain -->
      <security:custom-filter ref="usernamePasswordAuthenticationFilter" before="FORM_LOGIN_FILTER"/>
      <security:custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>
   </security:http>
   <!-- 用于認(rèn)證的 AuthenticationManager -->
   <security:authentication-manager alias="authenticationManager">
      <security:authentication-provider
         user-service-ref="userDetailsService"/>
      <security:authentication-provider ref="rememberMeAuthenticationProvider"/>
   </security:authentication-manager>

   <bean id="userDetailsService"
      class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
      <property name="dataSource" ref="dataSource" />
   </bean>

   <bean id="usernamePasswordAuthenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
      <property name="rememberMeServices" ref="rememberMeServices"/>
      <property name="authenticationManager" ref="authenticationManager"/>
      <!-- 指定 request 中包含的用戶名對(duì)應(yīng)的參數(shù)名 -->
      <property name="usernameParameter" value="username"/>
      <property name="passwordParameter" value="password"/>
      <!-- 指定登錄的提交地址 -->
      <property name="filterProcessesUrl" value="/login.do"/>
   </bean>
   <!-- Remember-Me 對(duì)應(yīng)的 Filter -->
   <bean id="rememberMeFilter"
   class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
      <property name="rememberMeServices" ref="rememberMeServices" />
      <property name="authenticationManager" ref="authenticationManager" />
   </bean>
   <!-- RememberMeServices 的實(shí)現(xiàn) -->
   <bean id="rememberMeServices"
   class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
      <property name="userDetailsService" ref="userDetailsService" />
      <property name="key" value="elim" />
      <!-- 指定 request 中包含的用戶是否選擇了記住我的參數(shù)名 -->
      <property name="parameter" value="rememberMe"/>
   </bean>
   <!-- key 值需與對(duì)應(yīng)的 RememberMeServices 保持一致 -->
   <bean id="rememberMeAuthenticationProvider"
   class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
      <property name="key" value="elim" />
   </bean>

需要注意的是 RememberMeAuthenticationProvider 在認(rèn)證 RememberMeAuthenticationToken 的時(shí)候是比較它們擁有的 key 是否相等,而 RememberMeAuthenticationToken 的 key 是 TokenBasedRememberMeServices 提供的,所以在使用時(shí)需要保證 RememberMeAuthenticationProvider 和 TokenBasedRememberMeServices 的 key 屬性值保持一致。需要配置 UsernamePasswordAuthenticationFilter 的 rememberMeServices 為我們定義好的 TokenBasedRememberMeServices,把 RememberMeAuthenticationProvider 加入 AuthenticationManager 的 providers 列表,并添加 RememberMeAuthenticationFilter 和 UsernamePasswordAuthenticationFilter 到 FilterChainProxy。

PersistentTokenBasedRememberMeServices

PersistentTokenBasedRememberMeServices 是 RememberMeServices 基于前文提到的持久化 token 的方式實(shí)現(xiàn)的。具體實(shí)現(xiàn)邏輯跟前文介紹的以 NameSpace 的方式使用基于持久化 token 的 Remember-Me 是一樣的,這里就不再贅述了。此外,如果單獨(dú)使用,其使用方式和上文描述的 TokenBasedRememberMeServices 是一樣的,這里也不再贅述了。

需要注意的是 PersistentTokenBasedRememberMeServices 是需要將 token 進(jìn)行持久化的,所以我們必須為其指定存儲(chǔ) token 的 PersistentTokenRepository。Spring Security 對(duì)此有兩種實(shí)現(xiàn),InMemoryTokenRepositoryImpl 和 JdbcTokenRepositoryImpl。前者是將 token 存放在內(nèi)存中的,通常用于測(cè)試,而后者是將 token 存放在數(shù)據(jù)庫(kù)中。PersistentTokenBasedRememberMeServices 默認(rèn)使用的是前者,我們可以通過(guò)其 tokenRepository 屬性來(lái)指定使用的 PersistentTokenRepository。

使用 JdbcTokenRepositoryImpl 時(shí)我們可以使用在前文提到的默認(rèn)表結(jié)構(gòu)。如果需要使用自定義的表,那么我們可以對(duì) JdbcTokenRepositoryImpl 進(jìn)行重寫。定義 JdbcTokenRepositoryImpl 時(shí)需要指定一個(gè)數(shù)據(jù)源 dataSource,同時(shí)可以通過(guò)設(shè)置參數(shù) createTableOnStartup 的值來(lái)控制是否要在系統(tǒng)啟動(dòng)時(shí)創(chuàng)建對(duì)應(yīng)的存入 token 的表,默認(rèn)創(chuàng)建語(yǔ)句為 “create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)”,但是如果自動(dòng)創(chuàng)建時(shí)對(duì)應(yīng)的表已經(jīng)存在于數(shù)據(jù)庫(kù)中,則會(huì)拋出異常。createTableOnStartup 屬性默認(rèn)為 false。

直接顯示地使用 PersistentTokenBasedRememberMeServices 和上文提到的直接顯示地使用 TokenBasedRememberMeServices 的方式是一樣的,我們只需要將上文提到的配置中 RememberMeServices 實(shí)現(xiàn)類 TokenBasedRememberMeServices 換成 PersistentTokenBasedRememberMeServices 即可。

   <!-- RememberMeServices 的實(shí)現(xiàn) -->
   <bean id="rememberMeServices"
   class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
      <property name="userDetailsService" ref="userDetailsService" />
      <property name="key" value="elim" />
      <!-- 指定 request 中包含的用戶是否選擇了記住我的參數(shù)名 -->
      <property name="parameter" value="rememberMe"/>
      <!-- 指定 PersistentTokenRepository -->
      <property name="tokenRepository">
         <bean class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
            <!-- 數(shù)據(jù)源 -->
            <property name="dataSource" ref="dataSource"/>
            <!-- 是否在系統(tǒng)啟動(dòng)時(shí)創(chuàng)建持久化 token 的數(shù)據(jù)庫(kù)表 -->
            <property name="createTableOnStartup" value="false"/>
         </bean>
      </property>
   </bean>