前兩天寫了一篇文章,主要講了下java中如何實現踢人下線,原文連接:java中如何踢人下線?封禁某個帳號後使其會話當即掉線!前端
原本只是簡單闡述一下踢人下線的業務場景和實現方案,沒想到引出那麼多大佬把小弟噴的睜不開眼睛,爲了不你們繼續噴我,特再寫下此篇文章,完全講清楚各類場景下踢人下線的設計思路,若有不足之處還請各位大佬輕噴!java
好了廢話很少說,正文開始算法
若是把踢人下線比喻成拆房子,那麼在學會拆房以前,咱們必需要了解這座房子是怎麼蓋起來的,不一樣的蓋法對應不一樣的拆法,不能混爲一談spring
對於目前大多數系統來說,登陸主要有兩種方式,一是傳統Session
模式,二是jwt令牌
模式數據庫
咱們先以Session模式
爲例,這種模式是怎麼登陸的呢?服務器
(注:此處的Session
不單指HttpSession
,指一切使用服務端控制會話的手段)session
這裏咱們不使用任何框架,從底層邏輯開始提及。app
首先,你須要一個全局攔截器,攔截全部會話請求,若是此會話已經登陸,那麼攔截器放行,若是未登陸,直接將此會話強制重定向到登陸接口框架
username + password
, 拿這兩個參數去數據庫中獲取數據用戶名或密碼錯誤
,若是能夠查找到數據,那麼開始登陸623368f0-ae5e-4475-a53f-93e4225f16ae
, 這就是咱們的tokentoken
與UserId
的映射關係,二是把這個token
返回給前端
Redis
中添加一條數據,假如userId=10001
,那麼咱們須要RedisUtil.set("623368f0-ae5e-4475-a53f-93e4225f16ae", 10001)
token
傳遞給前臺,你能夠放到Cookie
裏,或者直接放到返回體body
裏623368f0-ae5e-4475-a53f-93e4225f16ae
這個令牌,誰就是用戶10001
!token
且token有效,那麼會話放行!沒有?乖乖滾去登陸!此時不難看出,一個客戶端要保持會話登陸的兩個必要條件:spring-boot
token
token
是一個有效token
,即:能夠從Redis
中找到對應的UserId
而咱們要作踢人下線,就必須從這兩點至少選擇其一開始下手。
首先咱們先明確一點:除非客戶端主動註銷,不然咱們是沒法清除一個已經頒發到客戶端的token的。
(除了Cookie清除技術
和WebSocket實時推送技術
能夠作到,可是這兩種技術都須要客戶端主動配合,咱們如今的假設是客戶端拒不配合,咱們須要將它強制清退下線。)
如今,咱們只能從第二點下手,即:清除此token
與UserId
的映射關係
你可能會想,這不簡單?Redis
清除一個鍵值,還不是一行代碼就能解決的事情?
此時你可能漏掉了關鍵的一點,那就是,咱們只在Redis
中存儲了token -> UserId
的映射關係,若是咱們要踢出用戶10001
,正常狀況下,咱們沒法只根據10001
找到它對應的token
是哪一個鍵值
要解決這個問題,咱們就必須把UserId -> token
的映射關係也存儲一份,你能夠存儲在數據庫中,也能夠存儲在Redis
中,爲了性能考慮,咱們使用Redis
如今事情變得簡單起來,要踢人下線,咱們只須要兩步:
帳號10001
對應的token
鍵值OK,踢出成功,待到此帳號下一次訪問系統時,雖然他攜帶了token
,可是此token
已成爲無效token
,乖乖去登錄吧!
此時你可能會說:
就這?我建立個集合保存全部要踢出下線的帳號,每次攔截器裏判斷這個會話是否在這個集合中不就OK了?
大佬請慢噴!這就是我要說的第二種模式————黑名單機制,且往下看
jwt模式
的登錄步驟與傳統Session模式
區別不大,在此暫不贅述
不一樣點在於,jwt
登錄時,不會在服務器保存任何會話信息,全部的用戶參數都被寫進了jwt生成的token中
(因此jwt
的token
纔會長的那麼長!一般兩三百字符長度起步)
一個會話是否有效,只看這個會話攜帶的token
能不能正常解析出數據!
這也就意味着令牌的合法性是令牌自解釋的,而不是服務器說了算!
因此,相比於傳統Session模式
,jwt
對令牌的可控性就弱了不少,沒法作到主動清除token -> UserId
映射關係的操做
除非你手動更換jwt
令牌生成的算法祕鑰,可是這樣會形成系統中全部令牌所有失效,所有用戶集體下線!這是萬萬不行的。
那怎麼辦?難道我就不能作到踢人下線的操做嗎?
其實辦法確定是有的,只要思想不滑坡,方法總比困難多!
那就是利用黑名單機制:咱們要踢出哪一個用戶,只須要將他的UserId
或者jwt-token
放進一個黑名單裏,而後咱們在攔截器裏檢查每一個請求的token
或者UserId
是否存在於這個黑名單裏便可!
這種方式和傳統Session模式孰優孰劣呢?只能說各有千秋!
黑名單機制在存儲時節省性能,在攔截器裏多了一步黑名單檢測的步驟,浪費性能!
不過坦白了講,這丁點的性能的浪費對於如今的CPU來講都是毛毛雨,能夠直接忽略!
在我一位同事的項目中,給我提供了jwt踢人下線的另外一種實現思路:
那就是在生成jwt令牌時,加入一個固定的參數當作令牌生成因子
,若是要將一個用戶踢出下線,只須要修改一下這個因子的值,而後在攔截器裏每次校驗這個因子生成的令牌是否與客戶端傳遞的令牌一致!便可判斷出這個token是否已被拉黑!
這種模式提供了一個比較新穎的邏輯算法,可是嚴格來說,仍是藉助服務器存儲必定的數據完成的會話驗證,仍然屬於Session模式
。在此暫不展開細講。
說了這麼多理論,總歸是要上代碼的,因爲筆者除了sa-token框架
之外沒有找到任何一個框架對踢人下線有直接現成的解決方案,因此在此暫以sa-token框架
爲例
<!-- sa-token 權限認證, 在線文檔:http://sa-token.dev33.cn/ --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.12.1</version> </dependency>
@RestController @RequestMapping("user") public class UserController { @RequestMapping("doLogin") public String doLogin(String username, String password) { // 此處僅做示例模擬,真實項目須要從數據庫中查詢數據進行比對 if("zhang".equals(username) && "123456".equals(password)) { StpUtil.setLoginId(10001); return "登陸成功"; } return "登陸失敗"; } }
// 使指定id帳號的會話註銷登陸,對方再次訪問系統時會拋出`NotLoginException`異常,場景值爲-5 @RequestMapping("kickout") public String kickout(long userId) { StpUtil.logoutByLoginId(userId); return "踢出成功"; }
對框架感興趣的同窗能夠查看官網:sa-token 一個java輕量級權限認證框架
文章寫的再詳細也不免會有遺漏之處,在此還求你們輕噴,能夠在評論出留言指出不足之處
若是以爲文章寫得不錯還請你們不要吝惜爲文章點個贊,您的支持是我更新的最大動力!