前言
此篇隨筆記錄了Remember-Me實現過程中出現的問題和解決方案,以及相關的思考。
正文
1. RememberMe是什麼?
RememberMe意為記住我,對應登入介面的那個勾選項。另一種說法,就是自動登入。
2. 那什麼又是自動登入呢?
我們知道Tomcat或者其他Servlet容器的會話都是有時限的,比如Tomcat的會話時間為30分鐘,30分鐘後,會話將被清除,這時候就不滿足登入狀態了。那你發出下一個請求,按照正常邏輯就會被攔截下來,告知你沒有登入,然後跳轉到登入介面讓你重新登入。自動登入做的就是,當會話過期後,請求會根據Cookie資訊實現透明登入。也就是重新又登入了一遍,產生了一個新的會話。但是在使用者看來,他根本不知道發生了什麼,而且重新登入的過程中不需要再讓使用者輸入賬號密碼。
3. 為什麼不把會話時間調大一點呢?
Tomcat的會話時間是可配置的。你可以設定成3天或者更久。但是這樣就加大了伺服器的開銷。你設定3天,意味著一個HttpSession將會由Tomcat保留3天。(這裡到底是儲存在記憶體還是磁碟尚不確定,執行時記憶體的可能比較大),前面說了RemememberMe其實是自動登入,也就是說,它不會影響會話存在的時間。而且新會話的產生必然由新的請求觸發。
如果使用者登入後只瀏覽了10分鐘,就掛著不動了,在都沒有進行登出操作的情況下。對比一下兩種方案的最大開銷(服務端保留無用會話的時間)
第一種:3天會話時間 => 3天 - 10分鐘 => 3*24*60 - 10 = 4310 分鐘
第二種:30分鐘會話時間(預設) => 30 - 10 分鐘 = 20分鐘
4. RememberMe的原理是什麼?
本質上就是Cookie。在登入成功後,如果使用者有勾選“記住我”,則服務端在響應中加上Remember-Me的cookie,讓瀏覽器儲存一段時間,如7天。在7天之內,如果使用者會話過期,可憑此實現透明的重新登入。
5. Cookie中包含哪些內容呢?
1. 使用者賬號 => 不然無法知道誰要登入,而且需要依此獲取密碼,生成新簽名進行比對
2. 過期時間 => 過期的時間節點,即如果瀏覽器沒有及時自動清除此cookie,服務端收到後要據此刪除。
3. 與密碼有關的簽名 => 如果沒有密碼的相關資訊,那就很容易地通過偽造cookie,來登入其他人的賬號。但是密碼又不能是明文,必須經過加密。而且不能或者不能太容易被解開。
6. Spring Security中的Cookie格式
Base64(username:expireTime:MD5(username:expireTime:password:secretKey))
其中,簽名signature由username、expireTime、password、secretKey組成的字串,進過MD5加密而成。隨後再整合username、expireTime利用Base64編碼而成,Base64是可解碼的。最終結果,基本上就是一條亂碼了。
7. Cookie有了,服務端要怎麼處理呢?
那就是Filter的事情了。需要定義一個Remember Me的Filter專門處理這個Cookie。值得一提的是,校驗Cookie重新認證的過程還是要查資料庫的,所以應該做到只有在必要的時候,即會話過期的時候,才執行校驗操作。
8. Remember Me Filter與Login Filter的相容
Login Filter所做的就是,讓沒有登入的使用者,必須登入後才能處理請求。一般會直接返回錯誤碼,讓前端跳轉到登入頁,或者直接重定向到登入頁。所以Remeber Me Filter肯定要放置在Login Filter之前。即會話過期後,先進行的應該是自動登入。
9. 登出後,Cookie的刪除
使用者執行登出操作後,肯定要刪除瀏覽器的Cookie。否則,在Cookie過期之前,還是可以通過自動登入的方式,進入網站。但是,你不可能要求使用者去操作瀏覽器,刪除Cookie。這些操作應該由服務端完成。但是HTTP協議只有Set-Cookie的頭,卻沒有類似Remove-Cookie這樣的頭。HttpServletResponse也沒有明確的API可以刪除客戶端的Cookie。後來想到即可以通過新增不儲存的同名Cookie來覆蓋要刪除的Cookie,後來參考了下Security的實現,發現它也是這麼做的。程式碼如下:
public void logout(HttpServletRequest request, HttpServletResponse response) { //利用覆蓋的方法刪除客戶端的remember-me cookie Cookie cookie = new Cookie("remember-me", (String)null); cookie.setMaxAge(0); response.addCookie(cookie); request.getSession().invalidate(); }