初学websocket的小伙伴可能没有注意,websocket是需要身份认证的的。可能可以使用websocket发送一些简单的消息。但是假如说没有鉴权,不就意味着所有人都以进行消息发送。那么对于一个系统来说也太不安全了,所以需要在开启连接websocket的时候进行身份验证。 对应的在聊天系统中也需要思考好友的问题。比如只有好友之间才能互相发消息。不过我这个app目前的设计思路是面向公司的。默认一家公司的所有员工都是好友,可以互相进行通信。
本期对应视频,可从b站查看
目前已经写的文章有。并且有对应视频版本。
git项目地址 【IM即时通信系统(企聊聊)】点击可跳转
sprinboot单体项目升级成springcloud项目 【第一期】
前端项目技术选型以及页面展示【第二期】
分布式权限 shiro + jwt + redis【第三期】
给为服务添加运维模块 统一管理【第四期】
微服务数据库模块【第五期】
netty与mq在项目中的使用(第六期)】
分布式websocket即时通信(IM)系统构建指南【第七期】
分布式websocket即时通信(IM)系统保证消息可靠性【第八期】
分布式websocket IM聊天系统相关问题问答【第九期】
整体设计思路就是wesocket 建立连接发送消息的时候带上身份认证信息,将token带入。然后交由后台接口去进行身份认证。问题就出在了websocket该如何携带这个token呢,在netty自己构建的服务器中改如何使用呢,如何调用身份认证接口呢。后面将进行展开
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之间进行传递。
@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);
}
需要注意调用链路是在升级协议之前
/**
* 事件回调 在这个地方完成参数认证和授权.需要去调用一个接口去测试.
*
* @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() ;
}
本期视频主要实现了在websocket中如何进行权限的校验。具体校验逻辑可以自己实现,通过接口的方式来完成。
后面发视频的时候补一下。