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"
    ] 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值