Skip to main content

握手认证


握手认证

当前的握手

handShake.svg

现在握手还是需要三次才可以认证成功,我们能不能想办法变成两次,例如将第一次和第二次合并在一起,在升级连接的时候,就带着token去认证,这样就可以少一次网络开销

如下图所示:

handShake.svg

解决方案

new Websocket(url,protocols)

Websocket里有两个参数,分别是url和protocols

protocol传参

image-20240517225210166

当WebSocket请求获取请求头Sec-WebSocket-Protocol不为空时,需要返回给前端相同的响应,所以就需要处理,例如上面第二个框中,我们的protocols参数是token,这样后端也要返回相同的响应

我们现在netty的握手处理器在这里面:pipeline.addLast(new WebSocketServerProtocolHandler("/"));

追进去看代码

public void handlerAdded(ChannelHandlerContext ctx) {
    ChannelPipeline cp = ctx.pipeline();
    if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
        cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(), new WebSocketServerProtocolHandshakeHandler(this.serverConfig));
    }

    if (this.serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
        cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(), new Utf8FrameValidator(this.serverConfig.decoderConfig().closeOnProtocolViolation()));
    }

}

这里面有一个WebSocketServerProtocolHandshakeHandler,在这里面有channelRead函数,用来处理握手请求

final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
        getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()),
        serverConfig.subprotocols(), serverConfig.decoderConfig());

这里的参数是serverConfig.subprotocols(),这个应该是我们一开始配置好的,那么前端传入的token肯定不和这个一样,因此无法建立连接,如果想要用这种方式,就要把这里的代码全部复制出来,自己重新写

public class MyHandShakeHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
        final HttpObject httpObject = (HttpObject) msg;

        if (httpObject instanceof HttpRequest) {
            final HttpRequest req = (HttpRequest) httpObject;
            String token = req.headers().get("Sec-WebSocket-Protocol");
            try {
                final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                        req.getUri(),
                        token, false);
                final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
                if (handshaker == null) {
                    WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                } else {
                    ctx.pipeline().remove(this);
                    final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
                    handshakeFuture.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) {
                            if (!future.isSuccess()) {
                                ctx.fireExceptionCaught(future.cause());
                            } else {
                                ctx.fireUserEventTriggered(
                                        WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
                            }
                        }
                    });
                }
            } finally {
                ReferenceCountUtil.release(req);
            }
        } else {
            ReferenceCountUtil.release(msg);
        }
    }
}

此时我们可以拿到 String token = req.headers().get("Sec-WebSocket-Protocol");

并且也可以正确建立连接

image-20240517234933858

注意到代码中:

ctx.fireUserEventTriggered(
        WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);

后面会去触发握手完成的事件,那么我们就监听这个事件

public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {

        } else if (evt instanceof IdleStateEvent) {

        }else  if (evt instanceof WebSocketServerProtocolHandler.ServerHandshakeStateEvent) {
            log.info("握手成功请求");
        }
    }
}

image-20240517235331178

但是如果这时候再想去拿token是拿不到的,因为已经不是http请求了,无法从headers中获取,所以我们只能在之前的地方将token保存下来。我们可以使用ctx.channel().attr传递信息

在获取到token的地方:

            final HttpRequest req = (HttpRequest) httpObject;
            String token = req.headers().get("Sec-WebSocket-Protocol");
            Attribute<Object> token1 = ctx.channel().attr(AttributeKey.valueOf("token"));
            token1.set(token);

在监听事件的地方:

else  if (evt instanceof WebSocketServerProtocolHandler.ServerHandshakeStateEvent) {
    log.info("握手成功请求");
    Attribute<Object> token = ctx.channel().attr(AttributeKey.valueOf("token"));    
    if (token != null) {
        webSocketService.authorize(ctx.channel(), token.get().toString());
    }
}

url传参

new WebSocket('ws://localhost:8090?token=xxx')

在建立连接的时候,将token拼接在url里面

和上面做法类似,只不过我们现在在升级之前,从url参数中获取token:

public class MyHeaderollectHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.getUri());
            Optional<String> tokenOptional = Optional.of(urlBuilder).map(UrlBuilder::getQuery)
                    .map(k -> k.get("token"))
                    .map(CharSequence::toString);
            // 如果有token,就保存到channel中
            tokenOptional.ifPresent(s -> NettyUtils.setAttr(ctx.channel(), NettyUtils.TOKEN, s));
            //去掉token
            request.setUri(urlBuilder.getPath().toString());
        }
        // 触发责任链传递
        ctx.fireChannelRead(msg);
    }
}

取出token之后,我们要把url后面的参数都去掉,因为我们设置了pipeline.addLast(new WebSocketServerProtocolHandler("/"));

在握手的时候认证:

if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
    log.info("握手成功");
    String token = NettyUtils.getAttr(ctx.channel(), NettyUtils.TOKEN);
    if (StrUtil.isNotBlank(token)) {
        webSocketService.authorize(ctx.channel(), token);
    }
}