淺談踢人下線的設計思路!(附代碼實現方案)

前言

前兩天寫了一篇文章,主要講了下java中如何實現踢人下線,原文連接:java中如何踢人下線?封禁某個帳號後使其會話當即掉線!前端

原本只是簡單闡述一下踢人下線的業務場景和實現方案,沒想到引出那麼多大佬把小弟噴的睜不開眼睛,爲了不你們繼續噴我,特再寫下此篇文章,完全講清楚各類場景下踢人下線的設計思路,若有不足之處還請各位大佬輕噴!java

好了廢話很少說,正文開始算法

正文

若是把踢人下線比喻成拆房子,那麼在學會拆房以前,咱們必需要了解這座房子是怎麼蓋起來的,不一樣的蓋法對應不一樣的拆法,不能混爲一談spring

對於目前大多數系統來說,登陸主要有兩種方式,一是傳統Session模式,二是jwt令牌模式數據庫

傳統Session模式

咱們先以Session模式爲例,這種模式是怎麼登陸的呢?服務器

(注:此處的Session不單指HttpSession,指一切使用服務端控制會話的手段)session

這裏咱們不使用任何框架,從底層邏輯開始提及。app

首先,你須要一個全局攔截器,攔截全部會話請求,若是此會話已經登陸,那麼攔截器放行,若是未登陸,直接將此會話強制重定向到登陸接口框架

  1. 在登陸接口,咱們須要接受兩個參數:username + password, 拿這兩個參數去數據庫中獲取數據
  2. 若是查不到數據,直接返回用戶名或密碼錯誤,若是能夠查找到數據,那麼開始登陸
  3. 利用必定的算法(例如uuid),生成一個隨機字符串,就像這樣子:623368f0-ae5e-4475-a53f-93e4225f16ae, 這就是咱們的token
  4. 如今咱們須要作兩件事,一是創建此tokenUserId的映射關係,二是把這個token返回給前端
    1. 創建映射:在Redis中添加一條數據,假如userId=10001,那麼咱們須要RedisUtil.set("623368f0-ae5e-4475-a53f-93e4225f16ae", 10001)
    2. token傳遞給前臺,你能夠放到Cookie裏,或者直接放到返回體body
  5. 大工告成,會話登陸完畢!在全局攔截器裏,咱們不認userId只認token,誰持有623368f0-ae5e-4475-a53f-93e4225f16ae這個令牌,誰就是用戶10001
  6. 一個會話訪問進來,有token且token有效,那麼會話放行!沒有?乖乖滾去登陸!

此時不難看出,一個客戶端要保持會話登陸的兩個必要條件:spring-boot

  1. 此客戶端持有token
  2. 這個token是一個有效token,即:能夠從Redis中找到對應的UserId

而咱們要作踢人下線,就必須從這兩點至少選擇其一開始下手

首先咱們先明確一點:除非客戶端主動註銷,不然咱們是沒法清除一個已經頒發到客戶端的token的。

(除了Cookie清除技術WebSocket實時推送技術能夠作到,可是這兩種技術都須要客戶端主動配合,咱們如今的假設是客戶端拒不配合,咱們須要將它強制清退下線。)

如今,咱們只能從第二點下手,即:清除此tokenUserId的映射關係

你可能會想,這不簡單?Redis清除一個鍵值,還不是一行代碼就能解決的事情?

此時你可能漏掉了關鍵的一點,那就是,咱們只在Redis中存儲了token -> UserId的映射關係,若是咱們要踢出用戶10001,正常狀況下,咱們沒法只根據10001找到它對應的token是哪一個鍵值

要解決這個問題,咱們就必須把UserId -> token的映射關係也存儲一份,你能夠存儲在數據庫中,也能夠存儲在Redis中,爲了性能考慮,咱們使用Redis

如今事情變得簡單起來,要踢人下線,咱們只須要兩步:

  1. 找到帳號10001對應的token鍵值
  2. 刪除這個鍵值

OK,踢出成功,待到此帳號下一次訪問系統時,雖然他攜帶了token,可是此token已成爲無效token,乖乖去登錄吧!

此時你可能會說:

就這?我建立個集合保存全部要踢出下線的帳號,每次攔截器裏判斷這個會話是否在這個集合中不就OK了?

大佬請慢噴!這就是我要說的第二種模式————黑名單機制,且往下看

jwt模式

jwt模式的登錄步驟與傳統Session模式區別不大,在此暫不贅述

不一樣點在於,jwt登錄時,不會在服務器保存任何會話信息,全部的用戶參數都被寫進了jwt生成的token中

(因此jwttoken纔會長的那麼長!一般兩三百字符長度起步)

一個會話是否有效,只看這個會話攜帶的token能不能正常解析出數據!

這也就意味着令牌的合法性是令牌自解釋的,而不是服務器說了算!

因此,相比於傳統Session模式jwt對令牌的可控性就弱了不少,沒法作到主動清除token -> UserId 映射關係的操做

除非你手動更換jwt令牌生成的算法祕鑰,可是這樣會形成系統中全部令牌所有失效,所有用戶集體下線!這是萬萬不行的。

那怎麼辦?難道我就不能作到踢人下線的操做嗎?

其實辦法確定是有的,只要思想不滑坡,方法總比困難多!

那就是利用黑名單機制:咱們要踢出哪一個用戶,只須要將他的UserId或者jwt-token放進一個黑名單裏,而後咱們在攔截器裏檢查每一個請求的token或者UserId是否存在於這個黑名單裏便可!

這種方式和傳統Session模式孰優孰劣呢?只能說各有千秋!

黑名單機制在存儲時節省性能,在攔截器裏多了一步黑名單檢測的步驟,浪費性能!

不過坦白了講,這丁點的性能的浪費對於如今的CPU來講都是毛毛雨,能夠直接忽略!

題外話

在我一位同事的項目中,給我提供了jwt踢人下線的另外一種實現思路:

那就是在生成jwt令牌時,加入一個固定的參數當作令牌生成因子,若是要將一個用戶踢出下線,只須要修改一下這個因子的值,而後在攔截器裏每次校驗這個因子生成的令牌是否與客戶端傳遞的令牌一致!便可判斷出這個token是否已被拉黑!

這種模式提供了一個比較新穎的邏輯算法,可是嚴格來說,仍是藉助服務器存儲必定的數據完成的會話驗證,仍然屬於Session模式。在此暫不展開細講。

代碼實現方案?

說了這麼多理論,總歸是要上代碼的,因爲筆者除了sa-token框架之外沒有找到任何一個框架對踢人下線有直接現成的解決方案,因此在此暫以sa-token框架爲例

  1. 首先添加pom.xml依賴
<!-- 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>
  1. 在用戶登陸時將帳號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 "登陸失敗";
	}
}
  1. 將指定id的帳號踢出在線
// 使指定id帳號的會話註銷登陸,對方再次訪問系統時會拋出`NotLoginException`異常,場景值爲-5
@RequestMapping("kickout")
public String kickout(long userId) {
	StpUtil.logoutByLoginId(userId);
	return "踢出成功";
}

對框架感興趣的同窗能夠查看官網:sa-token 一個java輕量級權限認證框架

後話

文章寫的再詳細也不免會有遺漏之處,在此還求你們輕噴,能夠在評論出留言指出不足之處

若是以爲文章寫得不錯還請你們不要吝惜爲文章點個贊,您的支持是我更新的最大動力!

相關文章
相關標籤/搜索