基于Netty框架实现的WebSocket服务器握手认证传参笔记
Netty的WebSocketServerProtocolHandler负责处理WebSocket的握手,但具体的参数处理可能需要自定义Handler来处理HTTP请求。
认证参数传递方式
WebSocket握手基于HTTP协议,可通过以下方式传递认证参数:
URL Query参数:ws://host:port/path?token=xxx&user=123
HTTP Headers:Authorization
、X-Auth-Token
等自定义头
Cookies:在Cookie中存储认证信息
Protocal : 重写协议头传参
Netty实现流程
1 配置Pipeline
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// HTTP编解码器
pipeline.addLast(new HttpServerCodec());
// 聚合HTTP完整请求
pipeline.addLast(new HttpObjectAggregator(65536));
// URL Query参数
pipeline.addLast(new MyHttpHeaderHandler());
// 自定义认证处理器
// pipeline.addLast(new AuthHandler());
// WebSocket协议处理器
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 自定义handler ,处理业务逻辑
// pipeline.addLast(new MyHandShakeHandler());
// WebSocket业务处理器
pipeline.addLast(new WebSocketFrameHandler());
}
}
2 自定义认证处理器
URL Query参数
public class MyHttpHeaderHandler extends ChannelInboundHandlerAdapter {
/**
* 处理接收到的通道数据事件
* @param ctx 通道处理上下文,用于访问通道和事件流
* @param msg 接收到的消息对象,类型为Object
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof FullHttpRequest){
// 解析HTTP请求并处理URI参数
FullHttpRequest req = (FullHttpRequest) msg;
UrlBuilder urlBuilder = UrlBuilder.ofHttp(req.uri());
// 提取查询参数中的token值并设置到通道属性
Optional<String> tokenOptional = Optional.of(urlBuilder)
.map(UrlBuilder::getQuery)
.map(urlQuery -> urlQuery.get("token"))
.map(CharSequence::toString);
tokenOptional.ifPresent(s -> NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, s));
// 重置请求URI为路径部分(去除查询参数)
req.setUri(urlBuilder.getPath().toString());
}
// 继续传递事件到处理流水线的下一个环节
ctx.fireChannelRead(msg);
}
}
获取前端token
Channel channel = ctx.channel();
if(evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
webSocketService.connect(channel);
String token = NettyUtil.getAttr(channel, NettyUtil.TOKEN);
if(StrUtil.isNotBlank(token))
{
webSocketService.authorize(channel,new WSAuthorizeReq(token));
}
log.info("握手完成");
}
HTTP Headers
public class AuthHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 解析认证参数
String token = getToken(request);
String clientId = getClientId(request);
// 执行认证逻辑
if (!validateToken(token)) {
sendHttpResponse(ctx, request, new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED));
ctx.close();
return;
}
// 认证通过,存储用户信息
AttributeKey<String> clientKey = AttributeKey.valueOf("clientId");
ctx.channel().attr(clientKey).set(clientId);
// 传递修改后的请求给后续处理器
request.setUri("/ws"); // 清理query参数
ctx.fireChannelRead(request.retain());
}
private String getToken(FullHttpRequest request) {
// 从Query参数获取
QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
List<String> tokens = decoder.parameters().get("token");
if (tokens != null && !tokens.isEmpty()) {
return tokens.get(0);
}
// 从头信息获取
return request.headers().get("X-Auth-Token");
}
private boolean validateToken(String token) {
// 实现具体的token验证逻辑
return "valid_token".equals(token);
}
private void sendHttpResponse(...) {
// 发送HTTP响应实现
}
}
Protocal传参
@AllArgsConstructor
public class MyHandShakeHandler extends ChannelInboundHandlerAdapter {
/**
* WebSocket握手处理器,负责处理客户端的HTTP请求并完成WebSocket握手。
* 继承自ChannelInboundHandlerAdapter以处理入站事件。
*/
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
/**
* 处理接收到的通道读取事件,主要处理WebSocket握手逻辑。
*
* @param ctx 通道处理上下文,用于访问通道和事件派发
* @param msg 接收到的消息对象,预期为HTTP请求或相关对象
*/
HttpObject httpObject = (HttpObject) msg;
// 处理WebSocket握手逻辑
if (httpObject instanceof HttpRequest) {
final HttpRequest req = (HttpRequest) httpObject;
// 从请求头中提取WebSocket协议标识token
String token = req.headers().get("Sec-Websocket-Protocol");
// 将token保存到通道属性中以便后续使用
Attribute<Object> token1 = ctx.channel().attr(AttributeKey.valueOf("token"));
token1.set(token);
// 根据请求URI和协议创建WebSocket握手工厂
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);
// 执行握手并监听结果
ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
handshakeFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
// 监听握手完成事件,根据结果触发相应事件
if (!future.isSuccess()) {
ctx.fireExceptionCaught(future.cause());
} else {
ctx.fireUserEventTriggered(WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
}
}
});
}
} else {
// 转发非HTTP请求的消息到下一个处理器
ctx.fireChannelRead(msg);
}
}
}
调用
if(evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
log.info("握手完成");
Attribute<Object> token = channel.attr(AttributeKey.valueOf("token"));
webSocketService.authorize(channel,new WSAuthorizeReq(token.get().toString()));
}
关键点说明
Pipeline顺序:
认证处理器必须位于WebSocketServerProtocolHandler
之前
使用HttpObjectAggregator
确保接收完整HTTP请求
参数清理:
request.setUri("/ws"); // 移除query参数
避免敏感参数被后续处理器看到
用户信息传递:
// 存储
ctx.channel().attr(AttributeKey.valueOf("user")).set(userInfo);
// 在后续处理器获取
String user = ctx.channel().attr(AttributeKey.valueOf("user")).get();
错误处理:
返回标准HTTP状态码:
401 Unauthorized:认证失败
403 Forbidden:权限不足
及时关闭无效连接
客户端示例(JavaScript)
const ws = new WebSocket("ws://localhost:8080/ws?token=my_token");
// 或使用头信息
const ws = new WebSocket("ws://localhost:8080/ws", {
headers: {
"X-Auth-Token": "my_token"
}
});
或者直接控制台new一个
可以得到后端获取到前端传的token参数以及重新使用token获取用户信息成果
增强安全建议
使用加密连接:强制使用wss://
时效性验证:增加时间戳参数防止重放攻击
签名机制:对参数进行HMAC签名
限流控制:防止暴力破解
使用curl
模拟握手请求:
curl -i -N -H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Host: localhost:8080" \
-H "X-Auth-Token: invalid_token" \
http://localhost:8080/ws