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