目錄
- 知識要求
- 背景
- 技術原理
- 如何管理
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
就認為有登陸用戶。