如何在 Web PubSub for Socket.IO 中使用身份验证

背景

Socket.IO 协议是一种应用程序层协议,基于名为 Engine.IO 协议的传输层协议构建。 Engine.IO 负责在服务器和客户端之间构建低级别连接。 Engine.IO 连接只管理一个真实连接,或者是 HTTP 长轮询连接,或者是 WebSocket 连接。

Socket.IO 库提供的本机身份验证机制应用于 Socket.IO 连接级别。 身份验证生效之前,已成功构建 Engine.IO 连接。 客户端和服务器之间可以构建基础 Engine.IO 连接,而无需任何身份验证机制。 攻击者可以利用没有任何身份验证的 Engine.IO 连接,从而不受任何限制地使用客户的资源。

重要

本文中出现的原始连接字符串仅用于演示目的。

连接字符串包括应用程序访问 Azure Web PubSub 服务所需的授权信息。 连接字符串中的访问密钥类似于服务的根密码。 在生产环境中,请始终保护访问密钥。 使用 Azure Key Vault 安全地管理和轮换密钥,并使用 WebPubSubServiceClient 对连接进行保护

避免将访问密钥分发给其他用户、对其进行硬编码或将其以纯文本形式保存在其他人可以访问的任何位置。 如果你认为访问密钥可能已泄露,请轮换密钥。

Socket.IO 连接的身份验证

不建议在生产环境中使用此级别的身份验证。 因为它不会为低级别 Engine.IO 连接提供任何保护,这会让你的资源容易受到攻击。

Engine.IO 连接的身份验证

建议使用此级别的身份验证,因为它可以保护 Engine.IO 连接。 目前,Socket.IO 库不为 Engine.IO 连接提供这种身份验证机制。 Azure SocketIO 服务器 SDK 引入了协商机制,并提供要使用的 API。

在构建 Engine.IO 连接之前,客户端会向服务器发送包含身份验证信息的协商请求。 下面是该机制的详细工作原理:

  1. 在与服务终结点连接前,客户端会将携带身份验证所需信息的协商发送到服务器。
  2. 服务器将接收协商请求,分析身份验证信息,并根据分析的信息对客户端进行身份验证。 然后,服务器使用 JWT 令牌格式的访问令牌响应该请求。
  3. 客户端使用服务器提供的访问令牌连接到服务终结点。 访问令牌必须放置在使用 Socket.IO 请求的 access_token 命名的查询字符串中。
  4. 服务将会验证 access_token。 如果 access_token 无效,连接将被拒绝。

处理协商请求的 Web 应用程序可以是独立应用程序或 Socket.IO 应用程序的一部分。

基本用法

  • 服务器端
  1. 创建该服务支持的 Socket.IO 服务器

本文中出现的原始连接字符串仅用于演示目的。 在生产环境中,请始终保护访问密钥。 使用 Azure Key Vault 安全地管理和轮换密钥,并使用 WebPubSubServiceClient 对连接进行保护

const azure = require("@azure/web-pubsub-socket.io");
const app = express();
const server = require('http').createServer(app);

const io = require('socket.io')(server);
const wpsOptions = { hub: "eio_hub", connectionString: process.env.WebPubSubConnectionString };

azure.useAzureSocketIO(io, wpsOptions);
  1. negotiate 将 Socket.IO 服务器和 ConfigureNegotiateOptions 转换为完整的 Express 处理程序:
app.get("/negotiate", azure.negotiate(io, (req) => { userId: "user1" });)
  • 客户端
  1. 执行协商请求并分析结果
const negotiateResponse = await fetch(`/negotiate/?username=${username}`);
if (!negotiateResponse.ok) {
  console.log("Failed to negotiate, status code =", negotiateResponse.status);
  return ;
}
const json = await negotiateResponse.json();
  1. 让客户端使用协商响应中的信息与服务终结点连接。
var socket = io(json.endpoint, {
  path: json.path,
  query: { access_token: json.token }
});

聊天与协商中提供了完整的示例。

与 Passport 库集成

背景

在 Node.js 生态系统中,最主要的 Web 身份验证工作流是express + express-session + passport。 下面的列表解释了它们的角色:

  • express:后端框架
  • express-session:Express 团队支持的官方会话管理库。
  • passport:用于 Express 的身份验证包。 它专注于请求身份验证并支持超过 500 种身份验证策略,包括本地身份验证(用户名和密码)、OAuth (Google、GitHub、Facebook)、JWT、OpenID 等。
  • 身份验证成功后,passport 会提供一个描述通过身份验证的用户的对象。 此对象分配给 Express 请求变量中的 user 属性。 可以在随后的中间件中访问该属性。

使用情况

我们通过 userPassport 提供内置的 ConfigureNegotiateOptions,通过 restorePassport 提供中间件,以支持与 passport 集成。

Socket.IO 提供了一个示例,演示如何将 passport 身份验证与本机 Socket.IO 库配合使用。

此部分代码使用一组 Socket.IO 中间件将 passport 对象还原到请求中。

const io = require('socket.io')(server);

// convert a connect middleware to a Socket.IO middleware
const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);

io.use(wrap(sessionMiddleware));
io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));

io.use((socket, next) => {
  if (socket.request.user) {
    next();
  } else {
    next(new Error('unauthorized'))
  }
});

使用 useAzureSocketIO 启用服务后,开发人员应为 Express 应用添加协商处理程序。 usePassport 生成其 ConfigureNegotiateOptions。 然后,express restorePassport 应用作 Socket.IO 中间件,将 passport 对象还原为 socket.request

const io = require('socket.io')(server);

await useAzureSocketIO(io, { ...wpsOptions });

app.get("/negotiate", negotiate(io, usePassport()));

io.use(wrap(restorePassport()));

io.use(wrap(passport.initialize()));
io.use(wrap(passport.session()));

io.use((socket, next) => {
  if (socket.request.user) {
    next();
  } else {
    next(new Error('unauthorized'))
  }
});

此工作流不会还原会话对象。 Socket.IO 中间件无法访问会话。 socket.request.session 并不适用,它始终为 null。

// This usage will NOT work
io.use((socket, next) => {
  var session = socket.request.session; 
  // ... some code uses `session`
});

// This usage will NOT work
io.on('connect', (socket) => {
  const session = socket.request.session;
  // ... some code uses `session`
});

聊天-身份验证-passport 中提供了完整的示例。

重要

使用中间件的顺序错误可能导致身份验证工作流失败。 请遵循示例中给出的顺序,除非你清楚这些中间件的机制。