淺談踢人下線的設計思路!(附程式碼實現方案)
前言
前兩天寫了一篇文章,主要講了下java中如何實現踢人下線,原文鏈接:java中如何踢人下線?封禁某個帳號後使其會話立即掉線!
本來只是簡單闡述一下踢人下線的業務場景和實現方案,沒想到引出那麼多大佬把小弟噴的睜不開眼睛,為了避免大家繼續噴我,特再寫下此篇文章,徹底講清楚各種場景下踢人下線的設計思路,如有不足之處還請各位大佬輕噴!
好了廢話不多說,正文開始
正文
如果把踢人下線比喻成拆房子,那麼在學會拆房之前,我們必須要了解這座房子是怎麼蓋起來的,不同的蓋法對應不同的拆法,不能混為一談
對於目前大多數系統來講,登錄主要有兩種方式,一是傳統Session
模式,二是jwt令牌
模式
傳統Session模式
我們先以Session模式
為例,這種模式是怎麼登錄的呢?
(註:此處的Session
不單指HttpSession
,指一切使用服務端控制會話的手段)
這裡我們不使用任何框架,從底層邏輯開始說起。
首先,你需要一個全局攔截器,攔截所有會話請求,如果此會話已經登錄,那麼攔截器放行,如果未登錄,直接將此會話強制重定向到登錄介面
- 在登錄介面,我們需要接受兩個參數:
username + password
, 拿這兩個參數去資料庫中獲取數據 - 如果查不到數據,直接返回
用戶名或密碼錯誤
,如果可以查找到數據,那麼開始登錄 - 利用一定的演算法(例如uuid),生成一個隨機字元串,就像這樣子:
623368f0-ae5e-4475-a53f-93e4225f16ae
, 這就是我們的token - 現在我們需要做兩件事,一是建立此
token
與UserId
的映射關係,二是把這個token
返回給前端- 建立映射:在
Redis
中添加一條數據,假如userId=10001
,那麼我們需要RedisUtil.set("623368f0-ae5e-4475-a53f-93e4225f16ae", 10001)
- 將
token
傳遞給前台,你可以放到Cookie
里,或者直接放到返回體body
里
- 建立映射:在
- 大工告成,會話登錄完畢!在全局攔截器里,我們不認userId只認token,誰持有
623368f0-ae5e-4475-a53f-93e4225f16ae
這個令牌,誰就是用戶10001
! - 一個會話訪問進來,有
token
且token有效,那麼會話放行!沒有?乖乖滾去登錄!
此時不難看出,一個客戶端要保持會話登錄的兩個必要條件:
- 此客戶端持有
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模式
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框架
為例
- 首先添加pom.xml依賴
<!-- sa-token 許可權認證, 在線文檔://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.12.1</version>
</dependency>
- 在用戶登錄時將帳號id寫入會話中
@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的帳號踢出在線
// 使指定id帳號的會話註銷登錄,對方再次訪問系統時會拋出`NotLoginException`異常,場景值為-5
@RequestMapping("kickout")
public String kickout(long userId) {
StpUtil.logoutByLoginId(userId);
return "踢出成功";
}
對框架感興趣的同學可以查看官網:sa-token 一個java輕量級許可權認證框架
後話
文章寫的再詳細也難免會有遺漏之處,在此還求大家輕噴,可以在評論出留言指出不足之處
如果覺得文章寫得不錯還請大家不要吝惜為文章點個贊,您的支援是我更新的最大動力!