使用 Sa-Token 解決 WebSocket 握手身份認證

前言

相比于 Http 的單項通信方式代兵,WebSocket 可以從服務(wù)器向瀏覽器主動推送消息,這一特性可以幫助我們完成諸如 訂單消息推送涂邀、IM實時聊天 等一些特定業(yè)務(wù)替久。

然而 WebSocket 本身對“身份認證”并沒有提供直接的支持,對客戶端的連接默認是“來者不拒”,所以認證授權(quán)這個事清钥,得我們自己動手琼锋。

Sa-Token 是一個 java 權(quán)限認證框架,主要解決登錄認證祟昭、權(quán)限認證斩例、單點登錄、OAuth2从橘、微服務(wù)網(wǎng)關(guān)鑒權(quán) 等一系列權(quán)限相關(guān)問題。
GitHub 開源地址:https://github.com/dromara/sa-token

下面我們介紹一下如何在 WebSocket 中集成 Sa-Token 身份認證础钠,保證連接的安全性恰力。

兩種集成方式

我們將依次介紹目前最常見的兩種集成 WebSocket 方式:

  • Java 原生版:javax.websocket.Session
  • Spring 封裝版:WebSocketSession

廢話不多說,直接開搞:

方式一:Java 原生版 javax.websocket.Session

1旗吁、首先是引入 pom.xml 依賴
<!-- SpringBoot依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- WebScoket 依賴 -->
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-websocket</artifactId>  
</dependency>

<!-- Sa-Token 權(quán)限認證, 在線文檔:http://sa-token.dev33.cn/ -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.29.0</version>
</dependency>
2踩萎、登錄接口,用于獲取會話token
/**
 * 登錄測試 
 */
@RestController
@RequestMapping("/acc/")
public class LoginController {

    // 測試登錄  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
    @RequestMapping("doLogin")
    public SaResult doLogin(String name, String pwd) {
        // 此處僅作模擬示例很钓,真實項目需要從數(shù)據(jù)庫中查詢數(shù)據(jù)進行比對 
        if("zhang".equals(name) && "123456".equals(pwd)) {
            StpUtil.login(10001);
            return SaResult.ok("登錄成功").set("token", StpUtil.getTokenValue());
        }
        return SaResult.error("登錄失敗");
    }

    // ... 
    
}
3香府、WebSocket連接處理
@Component
@ServerEndpoint("/ws-connect/{satoken}")
public class WebSocketConnect {

    /**
     * 固定前綴 
     */
    private static final String USER_ID = "user_id_";
    
     /** 
      * 存放Session集合,方便推送消息 (javax.websocket.Session)  
      */
    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
    
    // 監(jiān)聽:連接成功
    @OnOpen
    public void onOpen(Session session, @PathParam("satoken") String satoken) throws IOException {
        
        // 根據(jù) token 獲取對應(yīng)的 userId 
        Object loginId = StpUtil.getLoginIdByToken(satoken);
        if(loginId == null) {
            session.close();
            throw new SaTokenException("連接失敗码倦,無效Token:" + satoken);
        }
        
        // put到集合企孩,方便后續(xù)操作 
        long userId = SaFoxUtil.getValueByType(loginId, long.class);
        sessionMap.put(USER_ID + userId, session);
        
        // 給個提示 
        String tips = "Web-Socket 連接成功,sid=" + session.getId() + "袁稽,userId=" + userId;
        System.out.println(tips);
        sendMessage(session, tips);
    }

    // 監(jiān)聽: 連接關(guān)閉
    @OnClose
    public void onClose(Session session) {
        System.out.println("連接關(guān)閉勿璃,sid=" + session.getId());
        for (String key : sessionMap.keySet()) {
            if(sessionMap.get(key).getId().equals(session.getId())) {
                sessionMap.remove(key);
            }
        }
    }
    
    // 監(jiān)聽:收到客戶端發(fā)送的消息 
    @OnMessage
    public void onMessage(Session session, String message) {
        System.out.println("sid為:" + session.getId() + ",發(fā)來:" + message);
    }
    
    // 監(jiān)聽:發(fā)生異常 
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("sid為:" + session.getId() + "推汽,發(fā)生錯誤");
        error.printStackTrace();
    }
    
    // ---------
    
    // 向指定客戶端推送消息 
    public static void sendMessage(Session session, String message) {
        try {
            System.out.println("向sid為:" + session.getId() + "补疑,發(fā)送:" + message);
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    // 向指定用戶推送消息 
    public static void sendMessage(long userId, String message) {
        Session session = sessionMap.get(USER_ID + userId);
        if(session != null) {
            sendMessage(session, message);
        }
    }
    
}
4、WebSocket配置
/**
 * 開啟WebSocket支持
 */
@Configuration  
public class WebSocketConfig { 
    
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();  
    }
    
} 
5歹撒、啟動類
@SpringBootApplication
public class SaTokenWebSocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(SaTokenWebSocketApplication.class, args); 
    }
    
}

搭建完畢莲组,啟動項目

6、測試

1暖夭、首先我們訪問登錄接口锹杈,拿到會話token

http://localhost:8081/acc/doLogin?name=zhang&pwd=123456

如圖所示:

令牌

2、然后我們隨便找一個WebSocket在線測試頁面進行連接
迈着,例如:https://www.bejson.com/httputil/websocket/

連接地址:

ws://localhost:8081/ws-connect/302ee2f8-60aa-42aa-8ecb-eeae5ba57015

如圖所示:

測試連接

3嬉橙、如果我們輸入一個錯誤的token,會怎樣呢寥假?

連接失敗

可以看到市框,連接會被立即斷開!

方式二:Spring 封裝版:WebSocketSession

1糕韧、同上:首先是引入 pom.xml 依賴
<!-- SpringBoot依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- WebScoket 依賴 -->
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-websocket</artifactId>  
</dependency>

<!-- Sa-Token 權(quán)限認證, 在線文檔:http://sa-token.dev33.cn/ -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.29.0</version>
</dependency>
2枫振、登錄接口喻圃,用于獲取會話token
/**
 * 登錄測試 
 */
@RestController
@RequestMapping("/acc/")
public class LoginController {

    // 測試登錄  ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
    @RequestMapping("doLogin")
    public SaResult doLogin(String name, String pwd) {
        // 此處僅作模擬示例,真實項目需要從數(shù)據(jù)庫中查詢數(shù)據(jù)進行比對 
        if("zhang".equals(name) && "123456".equals(pwd)) {
            StpUtil.login(10001);
            return SaResult.ok("登錄成功").set("token", StpUtil.getTokenValue());
        }
        return SaResult.error("登錄失敗");
    }

    // ... 
    
}
3粪滤、WebSocket 連接處理
/**
 * 處理 WebSocket 連接 
 */
public class MyWebSocketHandler extends TextWebSocketHandler {

    /**
     * 固定前綴 
     */
    private static final String USER_ID = "user_id_";
    
    /**
     * 存放Session集合斧拍,方便推送消息
     */
    private static ConcurrentHashMap<String, WebSocketSession> webSocketSessionMaps = new ConcurrentHashMap<>();

    // 監(jiān)聽:連接開啟 
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

        // put到集合,方便后續(xù)操作 
        String userId = session.getAttributes().get("userId").toString();
        webSocketSessionMaps.put(USER_ID + userId, session);
        

        // 給個提示 
        String tips = "Web-Socket 連接成功杖小,sid=" + session.getId() + "肆汹,userId=" + userId;
        System.out.println(tips);
        sendMessage(session, tips);
    }
    
    // 監(jiān)聽:連接關(guān)閉 
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 從集合移除 
        String userId = session.getAttributes().get("userId").toString();
        webSocketSessionMaps.remove(USER_ID + userId);
        
        // 給個提示 
        String tips = "Web-Socket 連接關(guān)閉,sid=" + session.getId() + "予权,userId=" + userId;
        System.out.println(tips);
    }

    // 收到消息 
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        System.out.println("sid為:" + session.getId() + "昂勉,發(fā)來:" + message);
    }

    // ----------- 
    
    // 向指定客戶端推送消息 
    public static void sendMessage(WebSocketSession session, String message) {
        try {
            System.out.println("向sid為:" + session.getId() + ",發(fā)送:" + message);
            session.sendMessage(new TextMessage(message));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    // 向指定用戶推送消息 
    public static void sendMessage(long userId, String message) {
        WebSocketSession session = webSocketSessionMaps.get(USER_ID + userId);
        if(session != null) {
            sendMessage(session, message);
        }
    }
    
}
4扫腺、WebSocket 前置攔截器
/**
 * WebSocket 握手的前置攔截器 
 */
public class WebSocketInterceptor implements HandshakeInterceptor {

    // 握手之前觸發(fā) (return true 才會握手成功 )
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler,
            Map<String, Object> attr) {
        
        System.out.println("---- 握手之前觸發(fā) " + StpUtil.getTokenValue());
        
        // 未登錄情況下拒絕握手 
        if(StpUtil.isLogin() == false) {
            System.out.println("---- 未授權(quán)客戶端岗照,連接失敗");
            return false;
        }
        
        // 標記 userId,握手成功 
        attr.put("userId", StpUtil.getLoginIdAsLong());
        return true;
    }

    // 握手之后觸發(fā) 
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception exception) {
        System.out.println("---- 握手之后觸發(fā) ");
    }
    
}
5笆环、WebSocket 配置
/**
 * WebSocket 相關(guān)配置 
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    // 注冊 WebSocket 處理器 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry
                // WebSocket 連接處理器 
                .addHandler(new MyWebSocketHandler(), "/ws-connect")
                // WebSocket 攔截器 
                .addInterceptors(new WebSocketInterceptor())
                // 允許跨域 
                .setAllowedOrigins("*");
    }

}
6攒至、啟動類
/**
 * Sa-Token 整合 WebSocket 鑒權(quán)示例 
 */
@SpringBootApplication
public class SaTokenWebSocketSpringApplication {

    public static void main(String[] args) {
        SpringApplication.run(SaTokenWebSocketSpringApplication.class, args); 
    }
    
}

啟動項目,開始測試

7躁劣、測試

1迫吐、首先訪問登錄接口,拿到會話token

http://localhost:8081/acc/doLogin?name=zhang&pwd=123456

如圖所示:

令牌

2账忘、然后打開WebSocket在線測試頁面進行連接
渠抹,例如:https://www.bejson.com/httputil/websocket/

連接地址:

ws://localhost:8081/ws-connect?satoken=fe6e7dbd-38b8-4de2-ae05-cda7e36bf2f7

如圖所示:

測試連接

注:這里采用 url 傳遞 Token 是因為在第三方測試頁面上這樣比較方便,真實項目中可以從Cookie闪萄、Header參數(shù)梧却、url參數(shù) 三種方式任選其一傳遞會話令牌,效果同等

3败去、如果輸入一個錯誤的 Token

連接失敗

連接失敺藕健!

示例地址

以上代碼已經(jīng)上傳git圆裕,示例地址:
碼云:sa-token-demo-websocket

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末广鳍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子吓妆,更是在濱河造成了極大的恐慌赊时,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件行拢,死亡現(xiàn)場離奇詭異祖秒,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門竭缝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來房维,“玉大人,你說我怎么就攤上這事抬纸×” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵湿故,是天一觀的道長阿趁。 經(jīng)常有香客問我,道長坛猪,這世上最難降的妖魔是什么脖阵? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮砚哆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屑墨。我一直安慰自己躁锁,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布卵史。 她就那樣靜靜地躺著战转,像睡著了一般。 火紅的嫁衣襯著肌膚如雪以躯。 梳的紋絲不亂的頭發(fā)上槐秧,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音忧设,去河邊找鬼刁标。 笑死,一個胖子當著我的面吹牛址晕,可吹牛的內(nèi)容都是我干的膀懈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谨垃,長吁一口氣:“原來是場噩夢啊……” “哼启搂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起刘陶,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤胳赌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后匙隔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疑苫,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缀匕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纳决。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖乡小,靈堂內(nèi)的尸體忽然破棺而出阔加,到底是詐尸還是另有隱情,我是刑警寧澤满钟,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布胜榔,位于F島的核電站,受9級特大地震影響湃番,放射性物質(zhì)發(fā)生泄漏夭织。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一吠撮、第九天 我趴在偏房一處隱蔽的房頂上張望尊惰。 院中可真熱鬧,春花似錦泥兰、人聲如沸弄屡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膀捷。三九已至,卻和暖如春削彬,著一層夾襖步出監(jiān)牢的瞬間全庸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工融痛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壶笼,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓雁刷,卻偏偏與公主長得像拌消,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子安券,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容