Spring Security 中的 RememberMe 登錄,so easy!
?1. RememberMe簡介
RememberMe 這個功能非常常見,圖 6-1 所示就是 QQ 郵箱登錄時的“記住我”選項。
提到 RememberMe,一些初學者往往會有一些誤解,認為 RememberMe 功能就是把用戶名/密碼用 Cookie 保存在瀏覽器中,下次登錄時不用再次輸入用戶名/密碼。這個理解顯然是不對的。
我們這里所說的 RememberMe 是一種服務器端的行為。傳統的登錄方式基于 Session 會話,一旦用戶關閉瀏覽器重新打開,就要再次登錄,這樣太過于煩瑣。如果能有一種機制,讓用戶關閉并重新打開瀏覽器之后,還能繼續保持認證狀態,就會方便很多,RememberMe 就是為了解決這一需求而生的。
具體的實現思路就是通過 Cookie 來記錄當前用戶身份。當用戶登錄成功之后,會通過一定的算法,將用戶信息、時間戳等進行加密,加密完成后,通過響應頭帶回前端存儲在 Cookie 中,當瀏覽器關閉之后重新打開,如果再次訪問該網站,會自動將 Cookie 中的信息發送給服務器,服務器對 Cookie 中的信息進行校驗分析,進而確定出用戶的身份,Cookie 中所保存的用戶信息也是有時效的,例如三天、一周等。敏銳的讀者可能已經發現這種方式是存在安全隱患的。所謂魚與熊掌不可兼得,要想使用便利,就要犧牲一定的安全性,不過在本章中,我們將會介紹通過持久化令牌以及二次校驗來降低使用 RememberMe 所帶來的安全風險。
2. RememberMe基本用法
我們先來看一種最簡單的用法。
首先創建一個 Spring Boot 工程,引入 spring-boot-starter-security 依賴。工程創建成功后,添加一個 HelloController 并創建一個測試接口,代碼如下:
然后創建 SecurityConfig 配置文件:
這里我們主要是調用了 HttpSecurity 中的 rememberMe 方法并配置了一個 key,該方法最終會向過濾器鏈中添加 RememberMeAuthenticationFilter 過濾器。
配置完成后,啟動項目,當我們訪問 /hello 接口時,會自動重定向到登錄頁面,如圖 6-2 所示。
可以看到,此時的默認登錄頁面多了一個 RememberMe 選項,勾選上 RememberMe,登錄成功之后,我們就可以訪問 /hello? 接口了。訪問完成后,關閉瀏覽器再重新打開,此時不需要登錄就可以直接訪問 /hello? 接口;同時,如果關閉掉服務端重新打開,再去訪問 /hello接口,發現此時也不需要登錄了。
那么這一切是怎么實現的呢?打開瀏覽器控制臺,我們來分析整個登錄過程。
首先,當我們單擊登錄按鈕時,多了一個請求參數 remember-me,如圖6-3所示。
很明顯,remember-me 參數就是用來告訴服務端是否開啟 RememberMe 功能,如果開發者自定義登錄頁面,那么默認情況下,是否開啟 RememberMe 的參數就是 remember-me。
當請求成功后,在響應頭中多出了一個 Set-Cookie,如圖 6-4 所示。
在響應頭中給出了一個 remember-me 字符串。以后所有請求的請求頭 Cookie 字段,都會自動攜帶上這個令牌,服務端利用該令牌可以校驗用戶身份是否合法。
大致的流程就是這樣,但是大家發現這種方式安全隱患很大,一旦 remember-me 令牌泄漏,惡意用戶就可以拿著這個令牌去隨意訪問系統資源。持久化令牌和二次校驗可以在一定程度上降低該問題帶來的風險。
3. 持久化令牌
使用持久化令牌實現 RememberMe 的體驗和使用普通令牌的登錄體驗是一樣的,不同的是服務端所做的事情變了。
持久化令牌在普通令牌的基礎上,新增了 series 和 token 兩個校驗參數,當使用用戶名/密碼的方式登錄時,series 才會自動更新;而一旦有了新的會話,token 就會重新生成。所以,如果令牌被人盜用,一旦對方基于 RememberMe 登錄成功后,就會生成新的 token,你自己的登錄令牌就會失效,這樣就能及時發現賬戶泄漏并作出處理,比如清除自動登錄令牌、通知用戶賬戶泄漏等。
Spring Security中對于持久化令牌提供了兩種實現:
- JdbcTokenRepositoryImpl
- InMemoryTokenRepositoryImpl
前者是基于 JdbcTemplate 來操作數據庫,后者則是操作存儲在內存中的數據。由于 InMemoryTokenRepositoryImpl 的使用場景很少,因此這里主要介紹基于 JdbcTokenRepositoryImpl 的配置。
首先創建一個 security06 數據庫,然后我們需要一張表來記錄令牌信息,創建表的 SQL 腳本在在 JdbcTokenRepositoryImpl? 類中的 CREATE_TABLE_SQL 變量上已經定義好了,代碼如下:
我們直接將變量中定義的 SQL 腳本拷貝出來到數據庫中執行,生成一張 persistent_logins 表用來記錄令牌信息。persistent_logins 表一共就四個字段:username 表示登錄用戶名、series 表示生成的 series 字符串、token 表示生成的 token 字符串、last_used 則表示上次使用時間。
接下來,在項目中引入 JdbcTemplate 依賴和 MySQL 數據庫驅動依賴:
然后在 application.properties 中配置數據庫連接信息:
最后修改 SecurityConfig:
在配置中我們提供了一個 JdbcTokenRepositoryImpl 實例,并為其配置了數據源,最后在配置 RememberMe 時通過 tokenRepository 方法指定 JdbcTokenRepositoryImpl 實例。
配置完成后,啟動項目并進行登錄測試。登錄成功后,我們發現數據庫表中多了一條記錄,如圖6-5所示。
此時如果關閉瀏覽器重新打開,再去訪問 /hello? 接口,訪問時并不需要登錄,但是訪問成功之后,數據庫中的 token? 字段會發生變化。同時,如果服務端重啟之后,瀏覽器再去訪問 /hello? 接口,依然不需要登錄,但是 token? 字段也會更新,因為這兩種情況中都有新會話的建立,所以 token? 會更新,而 series 則不會更新。當然,如果用戶注銷登錄,則數據庫中和該用戶相關的登錄記錄會自動清除。
可以看到,持久化令牌比前面的普通令牌安全系數提高了不少,但是依然存在風險。安全問題和用戶的使用便捷性就像一個悖論,想要用戶使用方便,不可避免地要犧牲一點安全性。對于開發者而言,要做的就是如何將系統存在的安全風險降到最低。
那么怎么辦呢?二次校驗可以幫助我們進一步降低風險.....