1. 程式人生 > >淺談踢人下線的設計思路!(附程式碼實現方案)

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

## 前言 前兩天寫了一篇文章,主要講了下java中如何實現踢人下線,原文連結:[java中如何踢人下線?封禁某個賬號後使其會話立即掉線!](https://juejin.cn/post/6919342604987727885) 本來只是簡單闡述一下踢人下線的業務場景和實現方案,沒想到引出那麼多大佬把小弟噴的睜不開眼睛,為了避免大家繼續噴我,特再寫下此篇文章,徹底講清楚各種場景下踢人下線的設計思路,如有不足之處還請各位大佬輕噴! 好了廢話不多說,正文開始 ## 正文 如果把踢人下線比喻成拆房子,那麼在學會拆房之前,我們必須要了解這座房子是怎麼蓋起來的,不同的蓋法對應不同的拆法,不能混為一談 對於目前大多數系統來講,登入主要有兩種方式,一是傳統`Session`模式,二是`jwt令牌`模式 #### 傳統Session模式 我們先以`Session模式`為例,這種模式是怎麼登入的呢? (注:此處的`Session`不單指`HttpSession`,指一切使用服務端控制會話的手段) 這裡我們不使用任何框架,從底層邏輯開始說起。 首先,你需要一個全域性攔截器,攔截所有會話請求,如果此會話已經登入,那麼攔截器放行,如果未登入,直接將此會話強制重定向到登入介面 1. 在登入介面,我們需要接受兩個引數:`username + password`, 拿這兩個引數去資料庫中獲取資料 2. 如果查不到資料,直接返回`使用者名稱或密碼錯誤`,如果可以查詢到資料,那麼開始登入 3. 利用一定的演算法(例如uuid),生成一個隨機字串,就像這樣子:`623368f0-ae5e-4475-a53f-93e4225f16ae`, 這就是我們的token 4. 現在我們需要做兩件事,一是建立此`token`與`UserId`的對映關係,二是把這個`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有效,那麼會話放行!沒有?乖乖滾去登入! 此時不難看出,一個客戶端要保持會話登入的兩個必要條件: 1. 此客戶端持有`token` 2. 這個`token`是一個有效`token`,即:可以從`Redis`中找到對應的`UserId` **而我們要做踢人下線,就必須從這兩點至少選擇其一開始下手**。 首先我們先明確一點:除非客戶端主動登出,否則我們是無法清除一個已經頒發到客戶端的token的。 (除了`Cookie清除技術`和`WebSocket實時推送技術`可以做到,但是這兩種技術都需要客戶端主動配合,我們現在的假設是客戶端拒不配合,我們需要將它強制清退下線。) 現在,我們只能從第二點下手,即:清除此`token`與`UserId`的對映關係 你可能會想,這不簡單?`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中 (所以`jwt`的`token`才會長的那麼長!通常兩三百字元長度起步) 一個會話是否有效,只看這個會話攜帶的`token`能不能正常解析出資料! 這也就意味著令牌的合法性是令牌自解釋的,而不是伺服器說了算! 所以,相比於`傳統Session模式`,`jwt`對令牌的可控性就弱了很多,無法做到主動清除`token -> UserId` 對映關係的操作 除非你手動更換`jwt`令牌生成的演算法祕鑰,但是這樣會造成系統中所有令牌全部失效,全部使用者集體下線!這是萬萬不行的。 那怎麼辦?難道我就不能做到踢人下線的操作嗎? 其實辦法肯定是有的,只要思想不滑坡,方法總比困難多! 那就是利用黑名單機制:我們要踢出哪個使用者,只需要將他的`UserId`或者`jwt-token`放進一個黑名單裡,然後我們在攔截器裡檢查每個請求的`token`或者`UserId`是否存在於這個黑名單裡即可! 這種方式和傳統Session模式孰優孰劣呢?只能說各有千秋! 黑名單機制在儲存時節省效能,在攔截器裡多了一步黑名單檢測的步驟,浪費效能! 不過坦白了講,這丁點的效能的浪費對於現在的CPU來說都是毛毛雨,可以直接忽略! #### 題外話 在我一位同事的專案中,給我提供了jwt踢人下線的另一種實現思路: 那就是在生成jwt令牌時,加入一個固定的引數當做令牌`生成因子`,如果要將一個使用者踢出下線,只需要修改一下這個因子的值,然後在攔截器裡每次校驗這個因子生成的令牌是否與客戶端傳遞的令牌一致!即可判斷出這個token是否已被拉黑! 這種模式提供了一個比較新穎的邏輯演算法,但是嚴格來講,還是藉助伺服器儲存一定的資料完成的會話驗證,仍然屬於`Session模式`。在此暫不展開細講。 #### 程式碼實現方案? 說了這麼多理論,總歸是要上程式碼的,由於筆者除了`sa-token框架`以外沒有找到任何一個框架對踢人下線有直接現成的解決方案,所以在此暫以`sa-token框架`為例 1. 首先新增pom.xml依賴 ``` java ``` 2. 在使用者登入時將賬號id寫入會話中 ``` java @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 "登入失敗"; } } ``` 3. 將指定id的賬號踢出線上 ``` java // 使指定id賬號的會話登出登入,對方再次訪問系統時會丟擲`NotLoginException`異常,場景值為-5 @RequestMapping("kickout") public String kickout(long userId) { StpUtil.logoutByLoginId(userId); return "踢出成功"; } ``` 對框架感興趣的同學可以檢視官網:[sa-token 一個java輕量級許可權認證框架](http://sa-token.dev33.cn/) #### 後話 文章寫的再詳細也難免會有遺漏之處,在此還求大家輕噴,可以在評論出留言指出不足之處 如果覺得文章寫得不錯還請大家不要吝惜為文章點個贊,您的支援是我更新的最大動力!

<