目錄
- 知識要求
- 背景
- 技術原理
- 如何管理
Session remember me的問題TODO- 附錄
知識要求
- 有一定的
WEB后端開發基礎,熟悉Session的用法,以及與Redis、Database的配合 - 本文的原理討論基於
PHP的Laravel,盡管原理是通用的,但是讀者具備相關知識理解會更輕松
背景
公司在業務層面上,通常會期望自己運營的系統,一個注冊帳號只能由本人使用或者少數幾個人共用。實際情況卻是:有些用戶會將注冊帳號的用戶名和密碼共享給成百上千個用戶;也有的用戶不直接提供用戶名和密碼,而是提供帶有認證信息的cookie給其他用戶,同樣達到共享賬號的目的。不論哪種形式,都造成了公司業務的損失。因此系統應該具備查看在線用戶的功能,並可對在線用戶實時管理,防止注冊帳號被許多人共享。
技術原理
在技術層面上,在線用戶都是用Session表示。
用戶通過用戶名和密碼正常登陸時,就會產生新的Session,退出則對應Session被清除。當多個用戶使用同一賬號登陸時,就會產生多個Session,防止共享賬號數量太多就是要限制同一賬號的Session數量。但是,這遠遠不夠。
我們都知道,Session的原理是通過將session id寫入cookie,下次瀏覽器訪問時會把cookie帶上來,識別其中的session id實現的(laravel存儲session_id的cookie名稱為laravel_session)。如果將該session id傳遞給其他用戶,其他用戶再將session id寫入cookie,就可以達到共享賬號,同時Session仍然是同一個的目的(見下圖流程)。這種情況使得在線用戶管理變得棘手,好在它們的IP並不相同,所以還是有辦法處理。

上面兩種形式是共享賬號的兩種主要形式,於是我們的問題就變成:
- 如何管理一個賬號對應多個
Session的問題 - 如何管理一個
Session多個IP的問題
下面先對如何管理Session做綜述,再對兩種情況分開說明。
如何管理Session
管理Session的前提是
- 系統能夠獲取到所有的
Session - 獲取
Session的所有IP信息。
一個高性能的系統,Session保存在緩存系統,比如Redis中。通過統一約定以session.開頭的鍵為Session,就可以獲取到所有Session,但這實際上是個很差的方法。Redis的設計並不是為了實現這樣的目的,所以它的鍵值匹配要么效率極低,要么不能保證返回所有結果,同時它的擴展性非常地差,比如只讀取某個用戶的所有Session,需要對鍵的命名再做進一步約束。
於是我們換了另外一種方案,Session仍然保存在緩存系統中,同時異步保存在數據庫中,注意必須是異步,否則會影響系統的運行。具體原理是,在Session的寫入、銷毀、回收這幾個階段發出event,將session連同HTTP請求放到隊列中(隊列是上下文無關的,獲取不到任何HTTP請求的信息,需要從事件中讀取),然后隊列取出這些事件,寫入到數據庫。這樣就能做到不影響性能,又可獲取到所有的Session信息,並做靈活地管理。
Session默認沒有攜帶IP信息,因此在每次Session寫入時,需要再做一層加工,將IP寫入Session,並且不能只保存一個IP,需要保存多個,以便后續問題的處理。
1.如何管理一個賬號對應多個Session的問題
既然數據庫中已經保存了所有Session,在有新的Session產生時,檢查是否超出指定數量。當超出時,自動刪除最早的Session即可。
如何手動測試
設置好要限制的數量,假設為2。安裝SessionBox(Chrome插件),創建3個窗口以相同用戶登陸,將發現最早登陸的窗口刷新后處於未登陸狀態。
合理的Session數量
一個用戶可能從多個設備登陸,比如PC、手機、平板,所以Session數量至少在3個以上。用戶也可能在PC上開N個不同瀏覽器,導致同一個設備有多個Session,應該優化此種情況,判定為同一個設備。具體看《TODO》這一節說明。
2.如何管理一個Session多個IP的問題
所有的用戶共享同一個Session,也就沒辦法精確控制要保留的數量了。要么刪除Session,所有用戶重新登陸;要么重新生成session id,只保留一個用戶,其他所有用戶需要重新登陸。目前采用后者,因為前者有一個風險,同一個用戶可能從多個地方登陸產生了大量的IP,結果就踢出去了,用戶體驗不好。而如果是后者,如果一個用戶,只是自己使用,那么不論他的ip數量是否突破限制,重新生成session id仍然是他的,所以不會受影響。
它的原理是用戶發起請求,發現IP過多,就重新生成session id,這個新的session id會寫到該用戶的cookie中,而其他用戶由於沒有這個新的session id,所以需要重新登陸。
從這個流程中也可看出,這個處理過程必須是在用戶發起請求時處理。需要注意的是,這個過程會需要考慮並發,即便是單個用戶訪問。假設一個用戶訪問頁面,該頁面同時發起4個請求。服務端同時處理這4個請求,都發現session的ip過多,於是刪除舊session重新生成,造成一個問題:4個請求刪除同一個舊session,然后生成了4個不同的session。為防止這種情況,使用了Redis對該session id加鎖,並設置30秒自動過期,只有第一個獲取鎖的人執行重新生成,其他沒獲得鎖的請求不處理。然后鎖不用釋放,自然過期即可。
正常而言,
session已經被重新生成了,舊session id是走不到加鎖session id這一步的。如果有,那一定是在重新生成之前就進來的請求,而這些請求本來就應該被忽略。反之,如果刪除鎖,這些請求將再次加鎖並重新生成session,仍然會造成剛才說的問題,因此直接讓鎖自動過期即可。
如何手動測試
假設IP數量限制為1個,打開A``B兩台電腦,在A電腦上先登陸,打開Chrome開發者工具,復制laraval_session的值;然后傳到B電腦,打開Chrome開發者工具,設置laravel_session的值,然后刷新下將發現變為登陸狀態。再刷新下A電腦,將發現處於未登陸狀態。

合理的ip限制數量
這個值則很主觀,同時多個用戶共享一個Session的問題實際是可避免的,具體參考《Remember Me的問題》的說明。因此不建議設置得太小,建議在5以上。
remember me的問題
如果一個賬號在線的存活期只有幾個小時,那么上面說的問題影響范圍都有限。為提高用戶體驗,用戶一次登陸后可存活好幾天甚至永久存活。提高存活時間有兩種方法:
- 修改
Session的過期時間 - 使用
Remember Me的機制
上面管理Session的方案,在第1種方法下能順利工作,但是在第2種方法下則沒法工作。所以使用了Remember Me的方案,在管理機制上需要重新設計。
要理解這個問題所在,我們需要理解Remember Me的機制:用戶登陸后,如果設置了Remember Me,服務器會生成remember me token,保存在數據庫中,並將該token寫入到cookie中。用戶Session過期后,再次訪問瀏覽器,服務端發現cookie中的remember me信息與數據庫中的一致,就重新生成新的Session(見流程圖)。

在了解上述機制后,即可發現,當remember me的用戶超過session限制數量后,最早的session被刪除,但由於該用戶有remember me,所以會重新生成session自動恢復,也就說,刪除session對remember me用戶無效,會立刻重新生成。所以上述的session管理方案不應該開啟remember me,否則是有問題的。
那為什么不直接Remember Me的方案呢?主要原因在於它的設計比較復雜,最終我們會切換成Remember Me的機制,將會另開一篇專門討論。
TODO
- [ ] 多個
Session可能是同一個設備發出的,因此應該結合IP判斷是否是同一個設備,或者結合客戶端發出的唯一標識(唯一標識需要是PC的唯一標識)。如果是同一個設備的就都放過的話,其實也有問題,SessionBox這樣的工具可以在單個瀏覽器創建多個Session,如果這個過程腳本化,服務器會產生大量垃圾Session,所以也應該限制數量。 - [ ] 改為
Remember me方案,將問題簡化為只考慮一個帳戶多個Session的問題。 - [ ] 客戶端發送唯一標識的方式是否具備可行性?
附錄
知識點
Q:Session與登陸用戶的關系?
A:嚴格來說,只要用戶打開瀏覽器訪問網站,就會產生Session標識一個會話,跟是否登陸無關。但是一般情況下,我們只關心登陸用戶的Session,因此這里討論上不做區分,只要產生Session就認為有登陸用戶。
