握手认证
握手认证
当前的握手
现在握手还是需要三次才可以认证成功,我们能不能想办法变成两次,例如将第一次和第二次合并在一起,在升级连接的时候,就带着token去认证,这样就可以少一次网络开销
如下图所示:
解决方案
new Websocket(url,protocols)
Websocket里有两个参数,分别是url和protocols
protocol传参
当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");
并且也可以正确建立连接
注意到代码中:
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("握手成功请求");
}
}
}
但是如果这时候再想去拿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);
}
}