實(shí)現(xiàn)記住我功能
1. 前言
「記住我」這一功能多出現(xiàn)在互聯(lián)網(wǎng)應(yīng)用中,其目的是為了減少用戶的認(rèn)證次數(shù)和訪問門檻。在一般的內(nèi)網(wǎng)應(yīng)用、或者是安全性要求較高的管理后臺(tái)中出現(xiàn)使用頻度較低。
「記住我 Remember-me」也稱為「持續(xù)登錄 Persistent-login」, 主要用到了 Cookies 和 Token 技術(shù),本節(jié)重點(diǎn)討論如何通過 Spring Security 配合出「記住我」的自動(dòng)認(rèn)證功能。
2. 記住我原理
「記住我」的核心思路是:將認(rèn)證狀態(tài)以安全的方式保存在客戶端。
「記住我」需要通過向?yàn)g覽器設(shè)置 Cookies 信息,這個(gè) Cookies 信息未來會(huì)用于建立會(huì)話連接,并且提供自動(dòng)登錄的能力。
「記住我」的基本流程為:
- 用戶通過瀏覽器登錄成功后,服務(wù)端生成一個(gè)可以持久化使用的 Token,并返回給瀏覽器;
- 瀏覽器端將該 Token 保存到 Cookies 中;
- 當(dāng)用戶離開應(yīng)用系統(tǒng),并再次返回,此時(shí)服務(wù)端由于沒有了該用戶的登錄會(huì)話,所以要求用戶再次登錄;
- 瀏覽器檢查 Cookies 中是否包含「記住我」的 Token,如有,將其發(fā)送給服務(wù)端;
- 服務(wù)端驗(yàn)證 Token,如果成功,直接返回登錄成功的結(jié)果。
3. 集成步驟
3.1 「記住我」Token 的存儲(chǔ)方式
通過前面描述我們看到,要實(shí)現(xiàn)「記住我」功能,關(guān)鍵在于如何安全的保護(hù)好用戶的認(rèn)證信息 Token。Spring Security 提供了兩種「記住我」的實(shí)現(xiàn)方式:
-
使用 Hash 算法加密認(rèn)證信息形成 Token,并將其保存在客戶端中;
-
將認(rèn)證信息保存在數(shù)據(jù)庫中,并將查詢條件保存在客戶端中。
3.1.1 基于 Hash 的方式
基于 Hash 的方式是一種相對簡單的集成方式。這種方式利用 Hash 的特性,將「記住我」信息進(jìn)行存檔。每當(dāng)用戶認(rèn)證通過,服務(wù)端便生成一條 Hash 記錄,并發(fā)送給客戶端瀏覽器,其中內(nèi)容包括「用戶名」、「Token 過期時(shí)間」、「密碼」、「簽名秘鑰」。
發(fā)送的具體內(nèi)容為:
base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: 根據(jù) UserDetailsService 配置得到用戶名信息。
password: 認(rèn)證密碼,確保 UserDetailsService 中可以匹配到目標(biāo)用戶。
expirationTime: 「記住我」Roken 的有效期,精確到毫秒。
key: 用于給 Token 簽名的密鑰信息,防止該 Token 被篡改。
發(fā)送出的 Token 只有到用戶下次需要登錄時(shí)才會(huì)被使用到,這期間,需要確保用戶名、密碼、密鑰等信息不被改變。還需要注意的是,「記住我」Token 在過期之前,可以在任何地方使用,因此其安全性上有一定的問題,如果使用數(shù)字認(rèn)證一樣。當(dāng)用戶認(rèn)為自己的 Token 不在安全時(shí),最好的辦法是立刻改變自己的認(rèn)證密碼,并且使全部的「記住我」Token 失效。
啟動(dòng)「記住我」功能僅需要一行配置,具體方式為:
<http>
...
<remember-me key="簽名密鑰"/>
</http>
當(dāng)有多個(gè) UserDetailsService 實(shí)例時(shí),可以通過 user-service-ref
屬性指定唯一實(shí)例。
3.1.2 基于存儲(chǔ)的方式
使用數(shù)據(jù)庫作為 Token 存儲(chǔ)方式,需要在 <remember-me>
配置中增加 data-source-ref
屬性,配置方式如下:
<http>
...
<remember-me data-source-ref="數(shù)據(jù)源實(shí)例"/>
</http>
所用到的數(shù)據(jù)源需要包含 persistent_logins
數(shù)據(jù)表,其結(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)
3.2 「記住我」相關(guān)接口及其實(shí)現(xiàn)
「記住我」需要配合「用戶名密碼認(rèn)證過濾器」一起使用,觸發(fā) RememberMeServices
實(shí)例實(shí)現(xiàn)其效果?!赣涀∥摇菇涌谥杏腥齻€(gè)主要方法,第一個(gè)名為 autoLogin
用于自動(dòng)登錄審核,另外兩個(gè)是 loginFail
和 loginSuccess
分別在認(rèn)證失敗或成功時(shí)觸發(fā)。
具體表現(xiàn)形式為:
// 自動(dòng)認(rèn)證
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
自動(dòng)認(rèn)證方法,在「記住我」功能啟用后,同時(shí)當(dāng)前上下文中找不到用戶信息時(shí)觸發(fā),我們需要根據(jù)不同的 Token 策略,實(shí)現(xiàn)「記住我」的判斷邏輯。
// 登錄失敗時(shí)觸發(fā)
void loginFail(HttpServletRequest request, HttpServletResponse response);
// 登錄成功時(shí)觸發(fā)
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
如前文所述,「記住我」有兩種 Token 策略,對應(yīng)了兩種實(shí)現(xiàn)方法。
3.2.1 基于 Hash 方式的實(shí)現(xiàn)
先上代碼:
<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>
<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>
<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>
基于 Hash 的方式需要配置三個(gè)核心 Bean 對象,分別是「過濾器」、「記住我處理服務(wù)」和「認(rèn)證管理器」。這其中 TokenBasedRememberMeServices
負(fù)責(zé)生成 Token 內(nèi)容,并交給「認(rèn)證管理器」使用。
最后,要把處理服務(wù) RememberMeServices
設(shè)置到用戶名密碼認(rèn)證過濾器 UsernamePasswordAuthenticationFilter.setRememberMeServices()
里,將記住我的認(rèn)證管理器添加到 AuthenticationManager.setProviders()
之中,將記住我過濾器添加到安全過濾鏈之中。
3.2.2 基于數(shù)據(jù)存儲(chǔ)方式的實(shí)現(xiàn)
使用數(shù)據(jù)存儲(chǔ)方式,其實(shí)現(xiàn)代碼與 Hash 方式基本相同,區(qū)別在于需要繼續(xù)配置 PersistentTokenRepository
來存取 Token,有兩個(gè)標(biāo)準(zhǔn)實(shí)現(xiàn)類:第一個(gè)是基于內(nèi)存的 InMemoryTokenRepositoryImpl
,第二個(gè)是基于 JDBC 的 JdbcTokenRepositoryImpl
。通常情況下,第一種用于集成測試,第二種用于生產(chǎn)環(huán)境。
4. 小結(jié)
本節(jié)我們討論了「記住我」的原理及快速集成方式:
- 「記住我」是一種基于 Token 的認(rèn)證形式;
- 「記住我」基于瀏覽器 Cookie 實(shí)現(xiàn),在瀏覽器中保存從服務(wù)端獲取的,用于下次認(rèn)證的 Token 內(nèi)容;
- 「記住我」是需要和用戶名密碼認(rèn)證方式同時(shí)出現(xiàn);
- 「記住我」有兩種 Token 策略,一種基于 Hash 值,另外一種基于數(shù)據(jù)庫持久化。
下節(jié)我們討論,當(dāng)系統(tǒng)對認(rèn)證有特殊需求且無法由 Spring Security 安全框架提供時(shí),如何實(shí)現(xiàn)使用外部方式認(rèn)證,使用 Spring Security 管理認(rèn)證結(jié)果及鑒權(quán)的方法。