使用 Sa-Token 完成踢人下線功能

一、需求

在企業(yè)級項目中,踢人下線是一個很常見的需求,如果要設計比較完善的話,至少需要以下功能點:

  • 可以根據(jù)用戶 userId 踢出指定會話,對方再次訪問系統(tǒng)會被提示:您已被踢下線,請重新登錄。
  • 可以查詢出一個賬號共在幾個設備端登錄,并返回其對應的 Token 憑證,以便后續(xù)操作。
  • 可以只踢出一個賬號某一個端的會話,其他端不受影響。例如在某電商APP上可以看到當前賬號共在幾個手機上登錄,并注銷指定端的會話,當前端不受影響。

手動從零開始設計滿足需求的會話架構(gòu),還是需要一定的代碼量的。本篇將介紹如何使用 Sa-Token 方便的完成上述需求,
Sa-Token 框架對踢人下線做了較為完整的封裝,我們可以使用極少的代碼就完成踢人下線功能。

Sa-Token 是一個輕量級 java 權(quán)限認證框架,主要解決登錄認證、權(quán)限認證、單點登錄、OAuth2、微服務網(wǎng)關鑒權(quán) 等一系列權(quán)限相關問題。
Gitee 開源地址:https://gitee.com/dromara/sa-token

首先在項目中引入 Sa-Token 依賴:

<!-- Sa-Token 權(quán)限認證 -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>

注:如果你使用的是 SpringBoot 3.x,只需要將 sa-token-spring-boot-starter 修改為 sa-token-spring-boot3-starter 即可。

二、踢人下線 API 一覽

先看看 Sa-Token 為我們提供的與踢人下線有關的API。

強制注銷:

StpUtil.logout(10001);                    // 強制指定賬號注銷下線 
StpUtil.logout(10001, "PC");              // 強制指定賬號指定端注銷下線 
StpUtil.logoutByTokenValue("token");      // 強制指定 Token 注銷下線 

踢人下線:

StpUtil.kickout(10001);                    // 將指定賬號踢下線 
StpUtil.kickout(10001, "PC");              // 將指定賬號指定端踢下線
StpUtil.kickoutByTokenValue("token");      // 將指定 Token 踢下線

強制注銷 和 踢人下線 的區(qū)別在于:

  • 強制注銷等價于對方主動調(diào)用了注銷方法,再次訪問會提示:Token無效。
  • 踢人下線不會清除Token信息,而是將其打上特定標記,再次訪問會提示:Token已被踢下線。

動態(tài)圖演示:

g3--kickout.gif

下面開始進行代碼實戰(zhàn)。

三、根據(jù)賬號踢人下線

在完成踢人下線之前,我們需要先讓會話完成登錄。正常的登錄需要根據(jù) username + password 判斷賬號合法性,由于我們本篇的重點是 踢人下線
所以此處簡化一下登錄操作,直接填入 userId 進行登錄。

package com.pj;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 測試踢人下線
 */
@RestController
@RequestMapping("/kick/")
public class KickController {

    // 會話登錄接口  ---- http://localhost:8081/kick/doLogin?id=10001
    @RequestMapping("doLogin")
    public SaResult doLogin(long userId) {
        StpUtil.login(userId);
        return SaResult.ok("登錄成功,Token 憑證為:" + StpUtil.getTokenValue());
    }

    // 驗證當前客戶端是否登錄  ---- http://localhost:8081/kick/checkLogin
    @RequestMapping("checkLogin")
    public SaResult checkLogin() {
        StpUtil.checkLogin();
        // 下面是登錄后才會返回的數(shù)據(jù)
        return SaResult.ok("您已登錄成功,userId=" + StpUtil.getLoginId());
    }

    // 根據(jù)賬號Id踢人下線  ---- http://localhost:8081/kick/kickout
    @RequestMapping("kickout")
    public SaResult kickout(long userId) {
        StpUtil.kickout(userId);
        return SaResult.ok("將賬號 " + userId + " 踢下線成功");
    }

    // 全局異常攔截
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace();
        return SaResult.error(e.getMessage());
    }

}

運行代碼,分別用三個獨立的瀏覽器測試登錄:

// 使用瀏覽器 1 測試登錄賬號 10001
http://localhost:8081/kick/doLogin?userId=10001

// 使用瀏覽器 2 測試登錄賬號 10002
http://localhost:8081/kick/doLogin?userId=10002

// 使用瀏覽器 3 測試登錄賬號 10003
http://localhost:8081/kick/doLogin?userId=10003

之所以使用三個獨立的瀏覽器來測試,是為了避免會話的相互覆蓋,造成邏輯不可控。訪問成功的話,服務端的返回信息會類似如下:

{
    "code": 200,
    "msg": "登錄成功,Token 憑證為:f53ac098-aed4-4de2-9223-8c3f1dab656d",
    "data": null
}

然后使用三個瀏覽器分別訪問登錄驗證接口:

http://localhost:8081/kick/checkLogin

返回信息如下:

{
    "code": 200,
    "msg": "您已登錄成功,userId=10001",
    "data": null
}

現(xiàn)在開始測試踢人下線,使用任意瀏覽器訪問:

http://localhost:8081/kick/kickout?userId=10002

返回信息:

{
    "code": 200,
    "msg": "將賬號 10002 踢下線成功",
    "data": null
}

賬號 10002 將被踢下線成功,現(xiàn)在我們再使用瀏覽器2 測試一下 10002 是否仍然在線:

{
    "code": 500,
    "msg": "Token已被踢下線:aa5911a6-3623-4fdb-98d0-055c46353981",
    "data": null
}

可以看到,10002會話已失效,無法通過登錄校驗。

四、根據(jù) Token 踢人下線

業(yè)務場景舉例:我要在APP上查看我的賬號共在幾個設備登錄,并且將除我之外的設備全部踢下線。

首先我們需要在 application.yml 中添加配置:

sa-token: 
    is-share: false

is-share 的含義是:在多人登錄同一賬號時,是否共用同一個 Token:

  • 此值為 true 時,所有登錄共用一個Token。
  • 此值為 false 時,每次登錄新建一個Token。

在以上 KickController 的基礎上,繼續(xù)添加接口:

/**
 * 測試踢人下線
 */
@RestController
@RequestMapping("/kick/")
public class KickController {

    // 其他代碼...
    
    // 以下是需要新添加的代碼

    // 查詢我的賬號已經(jīng)在幾個設備登錄  ---- http://localhost:8081/kick/tokenList
    @RequestMapping("tokenList")
    public SaResult tokenList() {
        long currUserId = StpUtil.getLoginIdAsLong();
        List<String> tokenList = StpUtil.getTokenValueListByLoginId(currUserId);
        return SaResult.data(tokenList);
    }

    // 根據(jù) Token 踢人下線  ---- http://localhost:8081/kick/kickoutToken?token=xxxx
    @RequestMapping("kickoutToken")
    public SaResult kickoutToken(String token) {
        StpUtil.kickoutByTokenValue(token);
        return SaResult.ok("將Token: " + token + " 踢下線成功");
    }

}

重啟項目(如果集成 Redis 了就清空 Redis數(shù)據(jù)一下),分別從三個獨立的瀏覽器測試訪問:

http://localhost:8081/kick/doLogin?userId=10001

返回如下:

{
    "code": 200,
    "msg": "登錄成功,Token 憑證為:450b8b73-f52d-4496-b67e-bdd579c8708a",
    "data": null
}

仔細觀察三個瀏覽器返回的信息,雖然三個瀏覽器都是登錄賬號 10001,但是每次返回的 Token 憑證都是不一樣的。

現(xiàn)在查詢一下當前賬號一共在幾個設備完成了登錄:

http://localhost:8081/kick/tokenList

返回如下:

{
    "code": 200,
    "msg": "ok",
    "data": [
        "450b8b73-f52d-4496-b67e-bdd579c8708a",
        "39d7974b-327d-4aea-a0b7-d90ab47caf0c",
        "d73c1bc5-d04f-4dc2-81ee-42c9438f9d78"
    ]
}

現(xiàn)在選一個 Token,將其踢下線:

http://localhost:8081/kick/kickoutToken?token=d73c1bc5-d04f-4dc2-81ee-42c9438f9d78

返回信息如下:

{
    "code": 200,
    "msg": "將Token: d73c1bc5-d04f-4dc2-81ee-42c9438f9d78 踢下線成功",
    "data": null
}

然后在對應的瀏覽器,驗證一下登錄狀態(tài):

http://localhost:8081/kick/checkLogin

返回如下:

{
    "code": 500,
    "msg": "Token已被踢下線:d73c1bc5-d04f-4dc2-81ee-42c9438f9d78",
    "data": null
}

可以看到,該設備登錄的會話已被踢下線。那么同賬號的其他設備有沒有受到影響呢,我們從其他瀏覽器驗證一下:

http://localhost:8081/kick/checkLogin

返回如下:

{
    "code": 200,
    "msg": "您已登錄成功,userId=10001",
    "data": null
}

可以看到,只有踢出的 Token 被強制下線了,其他端并沒有受到影響。


參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容