什么?websocket也有权限!这个应该怎么做?【第十期】

前言

初学websocket的小伙伴可能没有注意,websocket是需要身份认证的的。可能可以使用websocket发送一些简单的消息。但是假如说没有鉴权,不就意味着所有人都以进行消息发送。那么对于一个系统来说也太不安全了,所以需要在开启连接websocket的时候进行身份验证。 对应的在聊天系统中也需要思考好友的问题。比如只有好友之间才能互相发消息。不过我这个app目前的设计思路是面向公司的。默认一家公司的所有员工都是好友,可以互相进行通信。

对了,最近也开始玩小红书啦,上面有一些生活日常和自己遇到的面试题,小红书可以发图文所以有什么想法可以随时发,主要更新平台会在b站,小红书上面的东西也会整理成视频发在b站。关注一下我,才五个关注,可太惨了! 名字叫
治愈云(没鞋奔跑版)

本期对应视频,可从b站查看

目前已经写的文章有。并且有对应视频版本。
git项目地址 【IM即时通信系统(企聊聊)】点击可跳转
sprinboot单体项目升级成springcloud项目 【第一期】
前端项目技术选型以及页面展示【第二期】
分布式权限 shiro + jwt + redis【第三期】
给为服务添加运维模块 统一管理【第四期】
微服务数据库模块【第五期】
netty与mq在项目中的使用(第六期)】
分布式websocket即时通信(IM)系统构建指南【第七期】
分布式websocket即时通信(IM)系统保证消息可靠性【第八期】
分布式websocket IM聊天系统相关问题问答【第九期】

设计思路

整体设计思路就是wesocket 建立连接发送消息的时候带上身份认证信息,将token带入。然后交由后台接口去进行身份认证。问题就出在了websocket该如何携带这个token呢,在netty自己构建的服务器中改如何使用呢,如何调用身份认证接口呢。后面将进行展开

netty在http协议升级前截取参数

1.创建类
NettyWebSocketParamHandler
作用 截取参数

package com.netty.informationServe.serve.handler;

/**
 * @author rose
 * @create 2023/6/27
 */


import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.URLUtil;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;


/**
 * URL参数处理程序,这时候连接还是个http请求,没有升级成webSocket协议,此处SimpleChannelInboundHandler泛型使用FullHttpRequest
 *
 * @author Nanase Takeshi
 * @date 2022/5/7 15:07
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class NettyWebSocketParamHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    /**
     * 此处进行url参数提取,重定向URL,访问webSocket的url不支持带参数的,带参数会抛异常,这里先提取参数,将参数放入通道中传递下去,重新设置一个不带参数的url
     *
     * @param ctx     the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
     *                belongs to
     * @param request the message to handle
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        String uri = request.uri();
        log.info("NettyWebSocketParamHandler.channelRead0 --> : 格式化URL... {}", uri);
        Map<CharSequence, CharSequence> queryMap = UrlBuilder.ofHttp(uri).getQuery().getQueryMap();
        //将参数放入通道中传递下去
        AttributeKey<String> attributeKey = AttributeKey.valueOf("token");
        ctx.channel().attr(attributeKey).setIfAbsent(queryMap.get("token").toString());
        request.setUri(URLUtil.getPath(uri));
        ctx.fireChannelRead(request.retain());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        log.error("NettyWebSocketParamHandler.exceptionCaught --> cause: ", cause);
        ctx.close();
    }

}

这个地方将token截取下来并且在channel之间进行传递。

将上面类加入netty调用链

  @Autowired
    NettyWebSocketParamHandler nettyWebSocketParamHandler;
 @Override
    protected void initChannel(SocketChannel e) throws Exception {
        e.pipeline().addLast("http-codec", new HttpServerCodec()) //http编解码
        /**

         *HttpObjectAggregator 因为http在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合起来
         * 这就是为什么当浏览器发送大量数据时,会发出多次http请求
         */
                    .addLast("aggregator",new HttpObjectAggregator(65536)) //httpContent消息聚合
                    .addLast("http-chunked",new ChunkedWriteHandler())  // HttpContent 压缩
                /**

                 *WebSocketServerProtocolHandler 对应websocket,它的数据是以 帧(frame)形式 传递
                 * 可以看到 WebSocketFrame 下有六个子类
                 * 浏览器请求时,ws://localhost:7000/XXX 表示请求的资源
                 * 核心功能是 将http协议升级为ws协议,保持长连接
                 */
                    .addLast("nettyWebSocketParamHandler",nettyWebSocketParamHandler)
                    .addLast("protocolHandler",new WebSocketServerProtocolHandler("/websocket"))
//                    .addLast("nettyWebSocketHandler",nettyWebSocketHandler)
                    .addLast(new IdleStateHandler(READER_IDLE_TIME,
                            WRITER_IDLE_TIME,
                            ALL_IDLE_TIME,
                            TimeUnit.SECONDS))
                    .addLast("base_handler",myWebSocketHandler)
                    .addLast("register_handler",registerHandler)
                    .addLast("single_message",singleMessageHandler)
                     .addLast("ack_single_message",ackSingleMessageHandler)
                    .addLast("creat_group",creatGroupHandler)
                    .addLast("group_message",groupMessageHandler)
//                    .addLast(HeartBeatRequestHandler.INSTANCE)
                    .addLast(ExceptionHandler.INSTANCE);
    }


需要注意调用链路是在升级协议之前

在之后的调用链的userEventTriggered方法中写权限逻辑

 /**
     * 事件回调  在这个地方完成参数认证和授权.需要去调用一个接口去测试.
     *
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
            //协议握手成功完成
            log.info("NettyWebSocketHandler.userEventTriggered --> : 协议握手成功完成");
            //检查用户token
            AttributeKey<String> attributeKey = AttributeKey.valueOf("token");
            //从通道中获取用户token
            String token = ctx.channel().attr(attributeKey).get();
            log.info("NettyWebSocketHandler.userEventTriggered"+token);
//            if (token.equals("undefined")){
//                ctx.writeAndFlush(new CloseWebSocketFrame(400, "token 无效")).addListener(ChannelFutureListener.CLOSE);
//            }
//            ctx.fireChannelRead();
            RoseFeignConfig.token.set(token);
            GenericResponse auth = nettyMqFeign.getAuth();
            //先使用一个接口吧。后续添加个人有哪些权限的时候在做改进
            //校验token逻辑
            //......
//            if(1 == 2) {
//                //如果token校验不通过,发送连接关闭的消息给客户端,设置自定义code和msg用来区分下服务器是因为token不对才导致关闭
//
//            }
            if (auth.getStatusCode() == 200){
                //token校验通过
                log.info("token校验通过");
            }else{
//                ctx.writeAndFlush(new CloseWebSocketFrame(400, "token 无效")).addListener(ChannelFutureListener.CLOSE);
            }


      
        }

    }

这个地方是使用openfeign来远程调用的另一个服务的鉴权接口。另一个服务使用的shiro构成,使用的是jwt鉴权的方式。以往有视频讲到分布式jwt如何实现,理解这块可以看往期视频。然后后续那个权限的视频还会再发一个后续的,会更新一下refreshtoken这样的操作。

@FeignClient(value= "yan-loginUser",configuration = RoseFeignConfig.class)
public interface NettyMqFeign {

    @RequestMapping(value = "/auth/issuccess")
    public GenericResponse getAuth() ;
}

feign远程调用的时候请求头问题

这个地方还是使用了一下threadLocal。在websocket的回调函数里面
RoseFeignConfig.token.set(token);
将这个token设置进去。
然后看下面这个具体的类的apply方法,里面把这个token给传递下去;

package com.netty.informationServe.config;

import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;

/**
 * @author rose
 * @create 2024/1/6
 */

@Configurable
@Slf4j
public class RoseFeignConfig implements RequestInterceptor {

    /**
     * 当前使用数据源标识
     */
    public static ThreadLocal<String> token  = new ThreadLocal<String>();

    @Bean
    Logger.Level roseFeignConfigLog(){
        //这里记录所有,根据实际情况选择合适的日志level
        return Logger.Level.FULL;
    }
    //请求头携带token
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("token", token.get());
    }
}

测试效果图

本期视频主要实现了在websocket中如何进行权限的校验。具体校验逻辑可以自己实现,通过接口的方式来完成。

ws://127.0.0.1:88/websocket?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJzZXNzaW9uX2tleVwiOlwicVZYVmtkMTIzNDU2YjhZZXpRPT1cIn0iLCJvcGVuaWQiOiIxODc5ODc4LU5LQ05PLU5LTksiLCJpc3MiOiJyb3NlIiwiZXhwIjoxNzA2MDkzMTE0LCJ1c2VyaWQiOjEsImlhdCI6MTcwNjAwNjcxNCwianRpIjoiMTIzNDU2In0.uHoL-u54mArQR_BplpSWSGESQUCBVXJ0MbsYXM7_FPI

大家可以看到,目前链接是这样的哈;
在这里插入图片描述

会携带着token在后面然后进行身份认证。

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值