草庐IT

SpringBoot整合WebSocket和JWT(token)步骤以及注意事项

疯狂的头发 2023-07-23 原文

一、重点导读

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;
        }
    }

有关SpringBoot整合WebSocket和JWT(token)步骤以及注意事项的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby-on-rails - Rails 中的 NoMethodError::MailersController#preview undefined method `activation_token=' for nil:NilClass - 2

    似乎无法为此找到有效的答案。我正在阅读Rails教程的第10章第10.1.2节,但似乎无法使邮件程序预览正常工作。我发现处理错误的所有答案都与教程的不同部分相关,我假设我犯的错误正盯着我的脸。我已经完成并将教程中的代码复制/粘贴到相关文件中,但到目前为止,我还看不出我输入的内容与教程中的内容有什么区别。到目前为止,建议是在函数定义中添加或删除参数user,但这并没有解决问题。触发错误的url是http://localhost:3000/rails/mailers/user_mailer/account_activation.http://localhost:3000/rails/mai

  3. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  6. ruby-on-rails - 使用 HTTP.get_response 检索 Facebook 访问 token 时出现 Rails EOF 错误 - 2

    我试图在我的网站上实现使用Facebook登录功能,但在尝试从Facebook取回访问token时遇到障碍。这是我的代码:ifparams[:error_reason]=="user_denied"thenflash[:error]="TologinwithFacebook,youmustclick'Allow'toletthesiteaccessyourinformation"redirect_to:loginelsifparams[:code]thentoken_uri=URI.parse("https://graph.facebook.com/oauth/access_token

  7. ruby - cucumber 特征和步骤定义 - 2

    我是Cucumber测试的新手。我创建了两个特征文件:events.featurepartner.feature并将我的步骤定义放在step_definitions文件夹中:./step_definitions/events.rbpartner.rbCucumber似乎在所有.rb文件中查找步骤信息。有没有办法限制该功能查看特定的步骤定义文件?我之所以要这样做,是因为即使我使用了--guess标志,我也会遇到不明确的匹配错误。我之所以要这样做,有以下几个原因。我正在测试CMS,并希望在不同的功能中测试每种不同的内容类型(事件和合作伙伴)。事件.特征Feature:AddpartnerA

  8. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  9. ruby-on-rails - 设计通过 reset_password_token 获取用户 - 2

    我正在尝试创建密码规则来设计可恢复的密码更改。我通过passwords_controller.rb做了一个父类(superclass),但我需要在应用规则之前检查用户角色,但我所拥有的只是reset_password_token。 最佳答案 假设您的模型是用户:User.with_reset_password_token(your_token_here)Source 关于ruby-on-rails-设计通过reset_password_token获取用户,我们在StackOverflow

  10. ruby - token 认证 - 2

    简单代码require'net/http'url=URI.parse('getjson/otherdatahere[link]')req=Net::HTTP::Get.new(url.to_s)res=Net::HTTP.start(url.host,url.port){|http|http.request(req)}putsres.body只是想知道如何在phpcURL中放置身份验证token,我是这样做的    curl_setopt($ch,CURLOPT_HTTPHEADER,array('Authorization:Bearerxxx'));//Bearertokenfora

随机推荐