为 WebSocket 连接更新令牌的最佳做法是啥

Posted

技术标签:

【中文标题】为 WebSocket 连接更新令牌的最佳做法是啥【英文标题】:What's the best practice to renew a token for a WebSocket connection为 WebSocket 连接更新令牌的最佳做法是什么 【发布时间】:2017-05-22 21:41:12 【问题描述】:

这可能是基于意见的,但我仍然想知道是否有最佳做法,因为我对 websocket 做法几乎一无所知

我有一个 SPA,它从我自己的 OP 获取 JWT 令牌。然后它使用该 JWT 连接到我使用 REST 和 WebSockets 拥有的其他服务。

就 REST 而言,它非常简单:

REST API 验证 JWT(以 Authorization: Bearer ... 发送)并提供对受保护资源的访问权限或以 401 响应,让 SPA 知道它需要请求新令牌。

现在有了 websocket:

在加载 SPA 期间,一旦我得到一个令牌,我就会向我的 web 服务打开一个 WS。我发送的第一条消息是带有我的 JWT 的 login_message,然后我将其保存在服务器的 websocket 实例上以了解谁在发送消息。 我收到的每条后续消息都会验证 JWT 以查看它是否已过期。

据我了解,一旦过期,我将面临两种选择:

    删除带有某种token_expired 错误的 websocket,并在令牌刷新后强制浏览器建立新的 websocket 连接。

    保持 websocket 打开,返回错误消息并发送新的登录消息(一旦刷新令牌)

    不要使用登录消息,而只是在每个请求中发送 JWT。

问题:您会推荐哪种方法,为什么?在安全性和性能方面。还有其他我没有列出的常见做法吗?

【问题讨论】:

#1 `OP` 是什么意思? #2 经典方法是将用户/密码交换为新的 jwt。那么,我发送的第一条消息是我的 JWT 的 login_message 是什么意思 OP 与 OpenID 提供程序一样。我们当时正在尝试使用 JWT 对 WebSocket 应用身份验证,而不会打扰用户输入他的凭据,方法是在服务器端实现它或删除 websocket 并使用刷新的 JWT 重新授权。我将在一两天内详细分享我们提出的实践,因为我看到了对这个问题的如此兴趣。虽然使用 REST 一切都很容易,但 WS 保持套接字打开,因此需要解决令牌过期而套接字仍然打开的情况。 基本流程是什么? #1 用户进入 acme.com,输入其凭据,生成一个 jwt 令牌以能够使用您的任何 api,如果令牌过期,使用 websockets 生成一个新令牌?或者 #2 acme.com 没有登录表单,用户进入 acme.com,使用某种逻辑,该用户使用 websockets 获取 jwt 令牌,如果 jwt 过期,则使用 websockets 生成一个新令牌。 .......... 如果您的主要目标是拥有一个您的用户必须能够使用您的任何 api 的网络,我可以向您分享一个干净且标准的方法,您可以在其中使用 websockets(如果应用)在某些流程部分。 如上所述,用户通过使用标准 OAuth2.0 流程输入他的凭据来获取他的 JWT。使用该 JWT,他可以毫无问题地访问我的 REST API。然而,我的一项服务通过 WebSocket 提供实时数据,套接字也需要授权用户,就像使用 REST 一样,但是虽然 REST 是无状态的,但 WebSocket 保持打开状态,并且可能会达到短暂 JWT 的时间点过期。问题是关于这种特定的到期情况 - 如何更好地处理,通过关闭套接字或保持打开状态等待客户端使用应用程序代码更新 您的用户能否重新登录以更新令牌? 【参考方案1】:

我问了一个很老的问题,所以我很乐意分享我们选择的做法:

    一旦客户端第一次得到他的JWT(应用程序启动时),就会打开一个WebSocket。

    为了验证通道,我们发送一条我们定义为协议一部分的消息,称为authMessage,其中包含JWT

    服务器将此数据存储在套接字的实例中,并在通过网络发送数据或从客户端接收数据之前验证其有效性/到期。

    令牌在过期前几分钟内会在 Web 应用程序中静默刷新,并向服务器发出另一个 authMessage(从第 2 步开始重复)。

    如果由于某种原因它在更新之前过期,服务器将关闭该套接字。

这大致是我们在应用程序中实现的(没有优化)并且对我们非常有效。

【讨论】:

如果我理解正确,一旦 webapp 刷新令牌,您将新信息发送到服务器。然后服务器关闭旧连接并使用新的令牌信息打开一个新的套接字连接? 其实没有。它只是发送带有更新的 JWT 的新 authMessage,然后服务器端在套接字的实例上更新它。删除套接字是一项代价高昂的操作,因此我们希望将其保持在最低限度 - 仅用于刷新或过期 但这将允许客户端无需经过身份验证即可建立连接,对吗?您是否建议使用令牌作为路径参数进行第一次连接/重新连接,以确保不会为未经身份验证的客户端打开连接,还是不值得? @MarianKlühspies 好点。您绝对可以将此技术用于初始 WS 连接。这实际上是微不足道的实现而不是处理不会发送authMessage 的延迟套接字。请务必始终安全地执行此操作。关于这种方法有一篇很好的帖子:linode.com/docs/guides/authenticating-over-websockets-with-jwt【参考方案2】:

Oauth2 流程有两个选项来更新令牌。正如您所说,这些选项中的一个是提示消息,用于强制执行新的登录过程。

另一种选择是使用 refresh_token,您将在每次会话到期时避免对用户进行此登录过程并以静默方式更新令牌。

在这两种情况下,您都需要将 jwt 存储在客户端中(通常在登录后)并更新它(在交互式登录或静默重新生成后)。本地存储、存储或只是一个简单的全局变量是处理存储和更新客户端中的 jwt 的替代方法。

正如我们所见,jwt 再生是按照 oauth2 规范解决的,并在客户端执行,在您的情况下是 SPA。

所以下一个问题是:如何将这个 jwt(新的或更新的)传递给外部资源(经典的 rest api 或您的 websocket)?

经典的 Rest Api

在这种情况下,您可能知道,使用 http 标头发送 jwt 很容易。在每个 http 调用中,我们可以发送旧的/有效的 jwt 或更新的 jwt 作为标头,通常是Authorization: Bearer ...

Websocket

在这种情况下,这并不容易,因为根据快速审查,没有明确的方法来更新标题或任何其他“元数据”一旦建立连接

how to pass Authorization Bearer access token in websocket javascript client HTTP headers in Websockets client API

更重要的是,没有标头的概念,因此您需要使用以下方式将此信息(在您的情况下为 jwt)发送到您的 websocket:

协议
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);
cookies
document.cookie = 'MyJwt=' + jwt + ';'
var ws = new WebSocket(
    'wss://localhost:9000/wss/'
);
简单获取参数
var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");

Websocket 只接收文本

并且根据以下链接,websocket可以提取header,获取参数和协议就在稳定阶段

https://medium.com/hackernoon/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8 https://www.pubnub.com/blog/nodejs-websocket-programming-examples/ https://medium.com/@martin.sikora/node-js-websocket-simple-chat-tutorial-2def3a841b61

之后,websocket 服务器只接收文本:

const http = require('http');
const WebSocketServer = require('websocket').server;
const server = http.createServer();
server.listen(9898);
const wsServer = new WebSocketServer(
    httpServer: server
);
wsServer.on('request', function(request) 
    const connection = request.accept(null, request.origin);
    connection.on('message', function(message) 
      //I can receive just text 
      console.log('Received Message:', message.utf8Data);
      connection.sendUTF('Hi this is WebSocket server!');
    );
    connection.on('close', function(reasonCode, description) 
        console.log('Client has disconnected.');
    );
);

在每个请求中发送 jwt

在分析了前面的主题之后,将新的 o renew 令牌发送到您的 websocker 后端的唯一方法是在每个请求中发送它:

const ws = new WebSocket('ws://localhost:3210', ['json', 'xml']);
ws.addEventListener('open', () => 
  const data = 
         jwt: '2994630d-0620-47fe-8720-7287036926cd',
         message: 'Hello from the client!' 
     
  const json = JSON.stringify(data);
  ws.send(json);
);

未涵盖的主题

如何使用 refresh_token 执行 jwt 重新生成 如何处理静默再生

如果您需要这个未涵盖的主题,请告诉我。

【讨论】:

感谢您的详细回答,但它确实解决了正在处理静默再生的主题。我会用我们的实践来回答

以上是关于为 WebSocket 连接更新令牌的最佳做法是啥的主要内容,如果未能解决你的问题,请参考以下文章

处理中途断开连接的控制器的最佳做法是啥?

将 TFS 2017 更新 1 升级到 Azure DevOps Server 2019 更新 1.1 的最佳做法是啥?

关于主题名称的最佳做法是啥

使用 redux 在 react-native 应用程序中保存访问令牌的最佳实践是啥?

websocket 流式传输 交易订单更新

websocket 流式传输 交易订单更新