SpringBoot整合WebSocket和JWTtoken步骤以及注意事项

一、重点导读

1、拦截器的配置:由于WebSocket不能像http那样很简单的将token设置到请求头中,而基于token的拦截器基本都是在请求头中获取token,因此不能拦截WebSocket的请求,否则会报错空指针异常。token除了放在请求头,还能放在请求地址,因此可以采取路径变量或者使用?拼接在地址栏。用户信息的获取放在ChatEndpoint 中并根据token获取

2、ChatEndpoint 中如何获取token,使用路径变量+WebSocket的@PathParam注解

3、ChatEndpoint 中如何根据token获取当前的用户id

4、为了安全,用户id不要拼接在地址栏,如果后端使用前端传来的id,这很不安全,因为可能用户登录的账号id与地址栏中的id不同,这样用户就使用了别人的账号发送消息

二、代码

1、导入依赖

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

        <!--token-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
        </dependency>

2、在SpringBoot容器中注册WebSocket

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


@Configuration
public class WebSocketConfig {

    /**
     * 扫描注解了@ServerEndpoint注解的类
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

} 

3、端点类(核心逻辑)

关键代码:
@ServerEndpoint(value = "/chat/{token}")
User tokenUser = TokenUtils.getUser(token);

对应的请求:ws://localhost:9090/chat/{token}

如:ws://localhost:9090/chat/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMzAiLCJleHAiOjE2NjE0MDYzMjl9.XEktvzwASqWvBRkbFPPZ3cCntOxB4bPjOR4hjGpCuas

因为使用的token,因此数据没有选择去session中取,如果有需要session做法的小伙伴可以去参考b站WebSocket打造在线聊天室【完结】_哔哩哔哩_bilibili

import cn.hutool.json.JSONUtil;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 各个webSocket端点
 * 不拦截该请求,因为接口测试工具设置请求体困难,因此选择使用路径变量来传递token
 */
@ServerEndpoint(value = "/chat/{token}")
@Component
public class ChatEndpoint {


    /**
     * 用来储存在线用户的容器
     */
    public static Map<Integer, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

    /**
     * 用来给客户端发送消息
     */
    private Session session;


    /*建立时调用*/
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {

        //将当前session赋值给属性
        this.session = session;
        //从token获取用户数据
        User tokenUser = TokenUtils.getUser(token);
        Integer userId = tokenUser.getId();
        //将当前端点存放到onlineUsers中保存
        onlineUsers.put(userId, this);
        //系统消息推送所有在线用户给客户端
        //封装系统推送消息,前端onmessage接收的数据
        String message = MessageUtils.formatMessage(null, null, MessageType.TEXT, tokenUser.getName() + "已上线", true, null);
        sendMessageToAllUser(message);
        //查询并给客户端发送未读消息个数

    }


    /**
     * 接收到客户端发送的数据时调用
     *
     * @param message 客户端发送的数据
     * @param session session对象
     * @return void
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        Message msg = JSONUtil.toBean(message, Message.class);
        msg.setTime(new Date());
        //获取接收信息的用户
        Integer recipientId = msg.getRecipientId();
        //封装发送的消息
        String result = MessageUtils.formatMessage(msg);
        //发送消息
        Session toSession = onlineUsers.get(recipientId).getSession();
        sendMessage(toSession, result);
        //将消息存储在数据库

    }


    /**
     * 关闭时调用
     */
    @OnClose
    public void onClose(Session session, @PathParam("token") String token) {

        try {
            //从token获取用户数据
            User tokenUser = TokenUtils.getUser(token);
            Integer userId = tokenUser.getId();
            //从在线用户列表中移除
            onlineUsers.remove(userId);
            //广播
            String message = MessageUtils.formatMessage(null, null, MessageType.TEXT, tokenUser.getName() + "已下线", true, null);
            sendMessageToAllUser(message);
        } catch (Exception e) {

            e.printStackTrace();
        }

    }

    /**
     * 给所有的客户端发送消息
     *
     * @param message 给客户端发送消息
     * @return void
     */
    private void sendMessageToAllUser(String message) {
        //所有登录用户id
        Set<Integer> ids = onlineUsers.keySet();
        for (Integer id : ids) {
            //发送消息
            Session toSession = onlineUsers.get(id).getSession();
            sendMessage(toSession, message);
        }
    }

    /**
     * 发送消息给单个用户
     *
     * @param message
     */
    private void sendMessage(Session toSession, String message) {
        try {
            toSession.getBasicRemote().sendText(message);
        } catch (IOException e) {

            e.printStackTrace();
        }
    }

    public Session getSession() {
        return session;
    }
}

4、消息实体类以及消息工具类

package com.huayu.campuspostbar.eneity;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * 聊天消息实体类
 *
 * @TableName message
 */
@TableName(value = "message")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    /**
     *
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 发送者id
     */
    private Integer senderId;

    /**
     * 接收者
     */
    private Integer recipientId;

    /**
     * 消息类型,文本、图片、视频
     */
    private String type;

    /**
     * 内容
     */
    private String content;

    /**
     * 时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date time;
    /**
     * 是不是系统消息
     */
    private Boolean isSystemMessage;
    /**
     * 是否被读过
     */
    private Boolean haveRead;

    public Message(Integer senderId, Integer recipientId, String type, String content, Date time, Boolean isSystemMessage, Boolean haveRead) {
        this.senderId = senderId;
        this.recipientId = recipientId;
        this.type = type;
        this.content = content;
        this.time = time;
        this.isSystemMessage = isSystemMessage;
        this.haveRead = haveRead;
    }



}
package com.huayu.campuspostbar.utils;

import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

@Slf4j
@Component
public class TokenUtils {

    @Resource
    private UserMapper userMapper;

    private static UserMapper staticUserMapper;

    @PostConstruct
    public void init() {
        staticUserMapper = userMapper;
    }

    /**
     * 生成token
     *
     * @param user
     * @return
     */
    public static String getToken(User user) {
        return JWT.create().withExpiresAt(DateUtil.offsetDay(new Date(), 1))
                .withAudience(user.getId().toString())
                .sign(Algorithm.HMAC256(user.getPassword()));
    }

    /**
     * 获取token中的用户信息
     *
     * @return
     */
    public static User getUser() {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        try {
            String token = request.getHeader("token");
            String aud = JWT.decode(token).getAudience().get(0);
            Integer userId = Integer.valueOf(aud);
            User user = staticUserMapper.selectById(userId);//获取user信息
            user.setToken(token);//设置token
            return user;
        } catch (Exception e) {
            log.error("解析token失败", e);
            return null;
        }

    }

    /**
     * 获取token中的用户信息
     *
     * @return
     */
    public static User getUser(String token) {
        try {
            String aud = JWT.decode(token).getAudience().get(0);
            Integer userId = Integer.valueOf(aud);
            User user = staticUserMapper.selectById(userId);//获取user信息
            user.setToken(token);//设置token
            return user;
        } catch (Exception e) {
            log.error("解析token失败", e);
            return null;
        }

    }
}

5、token(JWT)的整合过程就不说了,大家可以去学习青戈大佬的视频,或者其他的文章。另外非常感谢青戈大佬的免费开源教程,真的教会了我很多的东西。

从0开始带你手撸一套SpringBoot+Vue后台管理系统(2022年最新版)_哔哩哔哩_bilibili

6、过滤器配置

不拦截WebSocket请求地址


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/",
                        "/chat/**",
                        "/user/register",
                        "/user/login",
                );   



    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }


}

7、token工具类中使用的核心方法:通过token获取user

 public static User getUser(String token) {
        try {
            String aud = JWT.decode(token).getAudience().get(0);
            Integer userId = Integer.valueOf(aud);
            User user = staticUserMapper.selectById(userId);//获取user信息
            user.setToken(token);//设置token
            return user;
        } catch (Exception e) {
            log.error("解析token失败", e);
            return null;
        }
    }

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...