WebSocket (连接前验证token)

用户连接服务器weksocket前,需经过jwt的token验证(token中包含账号信息),验证合法后,才可以于服务器正常交互。

实战

引入依赖

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

WebSocket配置类

重写modifyHandshake方法,从握手请求中提取token,并尝试从token中获取用户ID,然后将用户ID保存在WebSocket的session属性中。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

import com.keepc.common.utils.JwtHelper;
import com.keepc.common.utils.uuid.IdUtil;

import java.util.List;
import java.util.Map;

import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

/**
 * WebSocket配置类,用于配置WebSocket的相关设置。
 */
@Configuration
public class WebSocketConfig extends ServerEndpointConfig.Configurator {

    /**
     * 创建并返回一个ServerEndpointExporter的实例。
     * ServerEndpointExporter是Spring提供的用于注册WebSocket端点的组件。
     * 它会扫描并注册所有注解了@ServerEndpoint的类,使得这些WebSocket端点可以被服务器使用。
     *
     * @return ServerEndpointExporter实例
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 重写modifyHandshake方法,用于修改握手请求和响应。
     * 此方法会从握手请求中提取token,并尝试从token中获取用户ID,
     * 然后将用户ID保存在WebSocket的session属性中。
     *
     * @param sec      ServerEndpointConfig对象,用于获取用户属性
     * @param request  HandshakeRequest对象,用于获取请求头
     * @param response HandshakeResponse对象,用于设置响应头
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 初始化用户属性Map,可通过session.getUserProperties()获取
        final Map<String, Object> userProperties = sec.getUserProperties();
        // 从请求头中获取token
        Map<String, List<String>> headers = request.getHeaders();
        List<String> tokenHeaders = headers.get("token");
        String token = tokenHeaders.get(0);
        // 尝试从token解析用户id
        String userId = "";
        if (token != null) {
            userId = JwtHelper.getUserId(token);
        }

        // 将解析出的用户id或生成的未知用户id放入userProperties
        if (userId != null) {
            userProperties.put("userId", userId);
        } else {
            userProperties.put("unknownId", "未知用户" + IdUtil.fastSimpleUUID());
        }

    }
}

创建websocket的服务核心类

实现websocket的连接、释放、发送、报错等核心功能

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.keepc.system.config.WebSocketConfig;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket服务端,提供与客户端的实时通信能力。
 * 地址:ws://127.0.0.1:8800/ws
 */
@Component
@ServerEndpoint(value = "/ws", configurator = WebSocketConfig.class) // 指定WebSocketConfig配置
public class WebSocketServer {

    /**
     * 用于记录日志信息。
     */
    private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);

    /**
     * 保存所有在线客户端的Session,以支持消息广播和单点发送。
     */
    public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    /**
     * 当WebSocket连接打开时的处理逻辑。
     * 
     * @param session WebSocket会话对象,用于与客户端进行通信。
     * @param userId  通过URL路径参数传递的用户ID,用于标识用户。
     */
    @OnOpen
    public void onOpen(Session session) {
        // 进行用户连接验证
        final boolean isverify = openVerify(session);
        if (isverify) {
            // 验证通过,添加用户到在线列表
            String id = (String) session.getUserProperties().get("userId");
            sessionMap.put(id, session);
            log.info("用户ID为={}加入连接, 当前在线人数为:{}", id, sessionMap.size());
        } else {
            // 验证不通过,关闭连接
            try {
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 当WebSocket连接关闭时的处理逻辑。
     * 
     * @param session WebSocket会话对象。
     */
    @OnClose
    public void onClose(Session session) {
        // 从在线列表中移除断开连接的用户
        String id = (String) session.getUserProperties().get("userId");
        if (id == "" || id == null) {
            sessionMap.remove(id);
            log.info("有一连接正常关闭,移除username={}的用户session, 当前在线人数为:{}", id, sessionMap.size());
        } else {
            id = (String) session.getUserProperties().get("unknownId");
            sessionMap.remove(id);
            log.info("token验证不通过,移除username={}的用户session, 当前在线人数为:{}", id, sessionMap.size());
        }
    }

    /**
     * 当从客户端接收到消息时的处理逻辑。
     * 
     * @param message 客户端发送的消息。
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        // 向服务端发送消息,并进行日志记录
        String id = (String) session.getUserProperties().get("userId");
        log.info("服务端收到来自用户ID为={}的消息:{}", id, message);
        sendOneMessage(id, "服务端收到消息:" + message);
    }

    /**
     * 当WebSocket发生错误时的处理逻辑。
     * 
     * @param session WebSocket会话对象。
     * @param error   异常错误。
     */
    @OnError
    public void onError(Session session, Throwable error) {
        // 记录异常错误日志
        log.error("websocket发生异常错误:");
        error.printStackTrace();
    }

    /**
     * 广播消息到所有连接的客户端。
     * 
     * @param message 要广播的消息内容。
     */
    public void sendAllMessage(String message) {
        // 向所有在线用户广播消息
        log.info("【WebSocket消息】广播消息:" + message);
        Iterator<Entry<String, Session>> entries = sessionMap.entrySet().iterator();
        while (entries.hasNext()) {
            Entry<String, Session> entry = entries.next();
            Session toSession = entry.getValue();
            if (toSession.isOpen()) {
                try {
                    log.info("服务端给客户端[{}],用户{},发送消息{}", toSession.getId(), entry.getKey(), message);
                    toSession.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 向指定用户发送单点消息。
     * 
     * @param userId  目标用户的ID。
     * @param message 要发送的消息内容。
     */
    public void sendOneMessage(String userId, String message) {
        // 向指定用户发送消息
        Session toSession = sessionMap.get(userId);
        if (toSession != null && toSession.isOpen()) {
            try {
                synchronized (toSession) {
                    log.info("【WebSocket消息】单点消息:" + message);
                    toSession.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 向多个指定用户发送单点消息。
     * 
     * @param userIds 目标用户的ID列表。
     * @param message 要发送的消息内容。
     */
    public void sendMoreMessage(String[] userIds, String message) {
        // 向多个指定用户发送消息
        for (String userId : userIds) {
            sendOneMessage(userId, message);
        }
    }

    /**
     * 判断是否是合法用户。根据用户ID进行验证。
     * 
     * @param session WebSocket会话对象,用于获取用户ID。
     * @return 如果用户ID合法返回true,否则返回false。
     */
    public static boolean openVerify(Session session) {
        // 验证用户ID是否合法
        final String id = (String) session.getUserProperties().get("userId");
        if (id == "" || id == null) {
            return false;
        } else {
            return true;
        }
    }
}

Controller调用

import com.keepc.common.result.Result;
import com.keepc.system.webscoket.WebSocketServer;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/admin/system/ws")
@Api(tags = "WebSocket管理")
public class WebSocketController {

    @Autowired
    private WebSocketServer webSocketServer;

    // @PreAuthorize("hasAuthority('btn.ws.broadcas')")
    @ApiOperation(value = "广播信息")
    @PostMapping("/broadcas")
    public Result broadcasMessage(@RequestBody String message) {
        webSocketServer.sendAllMessage(message);
        return Result.ok();
    }

}

测试

使用postman连接
在这里插入图片描述

调用api发送广播消息

在这里插入图片描述

控制台输出日志

在这里插入图片描述

遇到的问题

websocket一直无法连接?

检查WebSocket配置类和核心服务类代码是否正确;是否使用过滤器、拦截器等组件拦截了请求;是否使用SpringSecurity等框架。修改配置放行 /ws请求。

过滤器放行参考

// 如果是websocket接口,直接放行
if ("/ws".equals(request.getRequestURI())) {
    chain.doFilter(request, response);
    return;
}

SpringSecurity放行参考

http.antMatchers("/ws").permitAll();

/**
 * 配置哪些请求不拦截
 * 排除swagger相关请求
 *
 * @param web WebSecurity对象
 * @throws Exception 异常情况
 */
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/favicon.ico", "/swagger-resources/**", "/webjars/**", "/v2/**",
            "/swagger-ui.html/**", "/doc.html", "/ws/**");
}

测试类无法启动?

测试类报错: Caused by: java.lang.IllegalStateException: jakarta.websocket.server.ServerContainer not available

使用随机端口启动Spring Boot的Web应用程序进行测试。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
/**
 * 使用随机端口启动Spring Boot的Web应用程序进行测试。
 * 这个注解配置使得测试时Spring Boot应用会随机选择一个端口来启动,避免了端口冲突的问题。
 * 适用于需要进行Web层测试的场景,例如RESTful服务的测试。
 */

扩展

案例消息以String类型为例,可自定义JSON消息规则。接受消息时根据规则判断即可。

{
    "type": 1, //消息类型
    "content": "Hello", //消息内容
    "send_id": "001", //消息发送人
    "accept_id": "", //消息接收人
    //消息接收组
    "accept_group": [
        "001",
        "002",
        "003"
    ] 
}
<think>好的,用户的问题是关于如何在WebSocket连接中添加带有Token的请求头。我需要先回顾一下相关的知识,并结合提供的引用内容来整理回答。 首先,根据引用内容,WebSocket协议在建立连接时使用HTTP升级机制。客户端和服务器之间通过握手过程建立连接,这时候可以设置自定义的请求头。但根据标准,WebSocket的API并不直接允许设置自定义的HTTP头,不过有一个特定的头Sec-WebSocket-Protocol可以用于传递子协议信息,用户可能利用这个头来传递Token。 引用1中提到,可以在客户端设置Sec-WebSocket-Protocol属性,并在服务器端取出这个头的值作为Token。例如,在JavaScript中创建WebSocket实例时,第二个参数可以传入一个包含子协议的数组,这里可以放入Token。然后在服务器端,比如Java的Servlet中,可以通过HttpServletRequest的getHeader方法获取这个头。 另外,引用2和3提到其他方法,比如在URL的查询参数中添加Token,或者在onopen事件后通过send方法发送Token。不过这些方法可能有安全风险或者需要额外的处理步骤。 需要注意的是,引用1还提到一个大坑,可能是指某些服务器或中间件对Sec-WebSocket-Protocol的处理需要特定的配置,否则可能拒绝连接。比如,服务器必须返回相同的Sec-WebSocket-Protocol头值,否则客户端会触发错误。因此,在服务器端设置响应头是必要的步骤。 接下来,我需要整理出步骤:客户端如何设置Sec-WebSocket-Protocol,服务器端如何获取,并注意响应头的设置。同时,要提到其他方法(如URL参数或send发送)作为替代方案,但指出优缺点。 还需要提醒用户关于安全性的问题,比如使用wss协议,避免敏感信息暴露在URL中,以及Token的加密处理。 最后,生成相关问题,可能涉及其他认证方式、处理多个子协议、安全措施等。</think>### 在WebSocket连接中添加Token请求头的实现方法 #### 1. 通过`Sec-WebSocket-Protocol`协议头传递(推荐方案) **客户端设置**: ```javascript var token = "your_token_here"; var ws = new WebSocket("ws://example.com/webSocketServer", [token]); // 第二个参数为子协议数组,可将token作为子协议传递[^1] ``` **服务端获取**(以Java为例): ```java // 在WebSocket握手阶段获取协议头 String token = ((HttpServletRequest) servletRequest).getHeader("Sec-WebSocket-Protocol");[^1] ``` **关键注意事项**: - 服务端必须在握手响应中包含相同的协议头: ```java response.setHeader("Sec-WebSocket-Protocol", token); ``` - 若服务端未返回该头,客户端会触发`onerror`事件 #### 2. 通过URL参数传递(备选方案) ```javascript var ws = new WebSocket("ws://example.com/webSocketServer?token=" + encodeURIComponent(token));[^3] ``` **优缺点**: - ✅ 兼容性更好 - ❌ 可能暴露敏感信息(需配合HTTPS) - ❌ 受URL长度限制 #### 3. 通过`onopen`事件发送(特殊场景使用) ```javascript ws.onopen = function() { ws.send(JSON.stringify({type: "auth", token: token}));[^2] } ``` **适用场景**:需双向认证或动态更新凭证 --- ### 安全性建议 1. **强制使用HTTPS**:始终使用`wss://`协议[^3] 2. **Token加密**:建议使用JWT等时效性令牌 3. **限制有效期**:设置短期有效的Token 4. **服务端验证**:需校验Token有效性后再建立持久连接 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值