Websockets客户端API中的HTTP头部信息

382

看起来使用支持自定义HTTP头的任何HTTP头客户端都很容易向您的WebSocket客户端添加自定义HTTP头,但我找不到如何在Web平台的 WebSocket API中执行此操作。

有人知道如何实现吗?

var ws = new WebSocket("ws://example.com/service");

具体来说,我需要能够发送一个HTTP授权头。


29
我认为一个好的解决方案是允许WebSocket在没有授权的情况下连接,但是在服务器上阻止并等待接收WebSocket的授权信息,在其onopen事件中传输授权信息。 - Motomotes
@Motes的建议似乎是最合适的。从onOpen中进行授权调用非常容易,允许您根据授权响应接受/拒绝套接字。我最初尝试在Sec-WebSocket-Protocol头中发送授权令牌,但感觉像个hack。 - BatteryAcid
1
@Motes 你好,能否解释一下“在服务器上阻塞并等待”的部分吗?你的意思是不处理任何消息,直到收到“auth”消息为止吗? - Himal
@Motes 感谢回复。我对阻塞部分有点困惑,因为据我所知,您无法阻止初始的 connect 请求。我在后端使用 Django channels,并将其设计为在 connect 事件上接受连接。然后,在 receive 事件中设置一个“is_auth”标志(如果它看到有效的身份验证消息)。如果未设置 is_auth 标志并且不是身份验证消息,则关闭连接。 - Himal
我想说的“block”是指拒绝任何其他流量,或者说是自己进行身份验证并拒绝任何失败的身份验证,就像你所说的那样。 - Motomotes
显示剩余7条评论
18个回答

373

更新2次

简短回答:不行,只能指定路径和协议字段。

更详细的回答:

在JavaScript的WebSockets API中没有为客户端/浏览器发送附加标头指定的方法。WebSocket构造函数可以指定HTTP路径(“GET /xyz”)和协议标头(“Sec-WebSocket-Protocol”)。

Sec-WebSocket-Protocol标头(有时扩展用于websocket特定的身份验证)是从WebSocket构造函数的可选第二个参数生成的:

var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);

以上代码的结果是以下标头:

Sec-WebSocket-Protocol: protocol

并且

Sec-WebSocket-Protocol: protocol1, protocol2
实现WebSocket身份验证和授权的常见模式是通过实施票证系统。其中,托管WebSocket客户端的页面会向服务器请求一张票,并在WebSocket连接设置期间将此票传递,在URL /查询字符串中,在协议字段中或作为连接建立后第一条消息的必需部分中传递此票。然后,如果票证有效(存在、尚未被使用、客户端IP编码在票证中匹配、票证中的时间戳是最近的等),则服务器仅允许继续进行连接。以下是WebSocket安全信息概要:https://devcenter.heroku.com/articles/websocket-security。基本身份验证曾经是一种选项,但已被弃用,即使指定了该标头,现代浏览器也不发送该标头。基本身份验证信息(已弃用-不再可用):

注意:以下信息在任何现代浏览器中均不再准确。

Authorization标头是由WebSocket URI的用户名和密码(或仅用户名)字段生成的:

var ws = new WebSocket("ws://username:password@example.com")

上述代码将生成以下带有字符串“username:password”进行base64编码的标头:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

我已经在Chrome 55和Firefox 50中测试了基本身份验证,并验证了基本身份验证信息确实与服务器协商(这在Safari上可能无法工作)。

感谢Dmitry Frank的基本身份验证答案


84
我也遇到了同样的问题。很遗憾这些标准集成得如此之差。你本以为他们会查看XHR API来找到WebSockets API的要求(因为WebSocket和XHR是相关的),但似乎它们只是将API单独开发在一个孤立的岛屿上。 - eleotlecram
4
@eleotlecram,请加入HyBi工作组并提出你的建议。该组对公众开放,正在进行协议后续版本的工作。 - kanaka
8
@Charlie:如果您完全控制服务器,那是一种选择。更常见的方法是从正常的HTTP服务器生成一个票据/令牌,然后让客户端发送该票据/令牌(可以作为websocket路径中的查询字符串或作为第一个websocket消息发送)。然后,websocket服务器会验证该票据/令牌是否有效(未过期、未被使用过、来自创建时的同一IP等)。此外,我相信大多数websocket客户端都支持基本认证(可能对您来说不够安全)。更多信息请查看:https://devcenter.heroku.com/articles/websocket-security - kanaka
3
我猜这是有意设计的。我认为实现故意借鉴了HTTP,但通过设计尽可能将它们分开。规范中的文本继续说:“然而,该设计并不限制WebSocket只能用于HTTP,未来的实现可以使用一个专用端口上更简单的握手协议,而不需要重新发明整个协议。最后一点很重要,因为交互式通信流量的模式与标准的HTTP流量模式不太相似,并且可能会在某些组件上产生异常的负载。” - user1441149
4
很遗憾,这似乎在Edge浏览器中无法使用。谢谢,微软 :/ - sibbl
显示剩余18条评论

88

这是一个备选方案,但现代浏览器会在连接时发送域Cookie,因此可以使用:

var authToken = 'R3YKZFKBVi';

document.cookie = 'X-Authorization=' + authToken + '; path=/';

var ws = new WebSocket(
    'wss://localhost:9000/wss/'
);

最终获得请求连接头:

Cookie: X-Authorization=R3YKZFKBVi

10
如果WS服务器URI与客户端URI不同,会发生什么? - Danish
13
好的,那不行,因为你不能在客户端为其他域设置cookie。 - Tofandel
5
但是,您可以设置一个HTTP服务,在相关路径上设置一个会话cookie,并在启动WebSocket之前调用它。例如,调用https://example.com/login并使响应在/wss上设置一个cookie,然后new WebSocket("wss://example.com/wss")将使用相关cookie开始握手请求。请注意,开发工具可能不会显示cookie,但它仍然应该被发送。 - Coderer
12
WebSocket请求不受同源策略限制。将身份验证作为cookie发送会使您的应用程序容易被劫持。请参见https://christian-schneider.net/CrossSiteWebSocketHijacking.html。 - Declan Shanaghy
5
Christian Schneider(上文的作者)建议在使用身份验证Cookie时,使用CSRF令牌来保护初始HTTP握手:ws = new WebSocket("wss://example.com/wss?csrftoken=<token>") - Alex Buchatski
显示剩余7条评论

42

无法发送Authorization头。

可以附加令牌查询参数。然而,在某些情况下,将主要的登录令牌作为明文查询参数发送可能是不想要的,因为这比使用头更不透明,并最终被记录在不确定的位置。如果这对您造成安全问题,另一种选择是仅用于Web Socket的第二个JWT令牌

创建一个REST端点来生成此JWT,当然只能由使用主要登录令牌(通过头传输)进行身份验证的用户访问。 Web Socket JWT可以与您的登录令牌配置不同,例如具有较短的超时时间,因此作为升级请求的查询参数发送更安全。

为您在其上注册SockJS eventbusHandler的相同路由创建单独的JwtAuthHandler。确保您的auth处理程序首先注册,以便您可以检查Web Socket令牌是否与后台中的用户相关联(JWT应该以某种方式与用户关联)。


这是我能想到的API Gateway WebSockets的唯一安全解决方案。Slack使用类似的RTM API,并设置了30秒超时时间。 - andrhamm
2
请注意,如果您正在从 Node.js 创建 WebSocket 连接(而不是浏览器),则可以发送授权标头。请参阅此处:https://github.com/apollographql/apollo-client/issues/3967#issuecomment-882338049 - Venryx
所以,你的意思是为了对WebSocket进行身份验证,而不是添加设置标头的功能,你需要复制整个身份验证堆栈?我有什么地方理解错了吗? - undefined

41

HTTP授权头问题可以通过以下方式解决:

var ws = new WebSocket("ws://username:password@example.com/service");

然后,将使用提供的 usernamepassword 设置适当的基本身份验证 HTTP 标头。如果需要基本身份验证,则已完成设置。


我想使用 Bearer,因此我采用以下小技巧:我按如下方式连接到服务器:

var ws = new WebSocket("ws://my_token@example.com/service");

当我的服务器端代码接收到带有非空用户名和空密码的基本授权头时,将把用户名解释为令牌。


15
我正在尝试你建议的解决方案,但我无法看到授权标头被添加到我的请求中。我已经尝试了不同的浏览器,如Chrome V56,Firefox V51.0。我正在本地主机上运行我的服务器,因此websocket URL为"ws://myusername:mypassword@localhost:8080/mywebsocket"。有什么想法可能出了什么问题?谢谢。 - LearnToLive
6
通过URL转移令牌安全吗? - Ilya Loskutov
9
我同意@LearnToLive的观点 - 我使用了wss://user:password@myhost.com/ws(例如)并且在服务器端没有收到Authorization头信息 (使用Chrome版本60)。 - user9645
6
我和@LearnToLive以及@user9645遇到了同样的问题。当URI采用"wss://user:pass@host"格式时,无论是chrome还是firefox都没有添加授权头信息。这是浏览器不支持该格式,还是握手出了问题? - David Kaczynski
14
使用这种形式的URL http://username:password@example.com 已经过时。可能这也是它不能与WebSockets一起使用的原因。https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication。 - Dachstein
显示剩余6条评论

31

使用JavaScript WebSockets API建立WebSockets连接时,无法发送自定义头信息。 您可以通过使用第二个WebSocket类构造函数来使用Subprotocols头信息:

var ws = new WebSocket("ws://example.com/service", "soap");

然后您可以在服务器上使用Sec-WebSocket-Protocol键获取子协议标头。

还有一个限制,您的子协议标头值不能包含逗号(,)!


1
一个 JWT 可以包含逗号吗? - CESCO
1
我不这么认为。JWT包含三个由句点分隔的base64编码负载,我相信这就排除了逗号的可能性。 - BillyBBone
3
我已经实现了这个功能,它可以正常工作,只是感觉有点奇怪。谢谢。 - BatteryAcid
12
您是在建议我们使用“Sec-WebSocket-Protocol”头部作为“Authorization”头部的替代吗? - rrw

31

你无法添加标题,但如果你只需要在连接时将值传递给服务器,可以在URL上指定查询字符串:

var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");

那个URL是有效的,但是 - 当然 - 你需要修改你的服务器代码来解析它。


32
需要小心使用这个解决方案,查询字符串可能会被拦截、记录在代理服务器等地方。因此,通过这种方式传递敏感信息(如用户/密码/身份验证令牌)将不足以确保安全性。 - Nir
8
使用WSS时,查询字符串应该是安全的。 - Sebastien Lorber
13
WS 是纯文本。使用 WS 协议,任何内容都可以被拦截。 - Gabriele Carioli
1
@SebastienLorber,使用查询字符串是不安全的,因为它没有被加密。虽然HTTPS也是如此,但由于使用了“ws://…”协议,所以这并不重要。 - Lu4
16
@Lu4 查询字符串已加密,但是有很多其他原因不应该将敏感数据添加为URL查询参数。请参考以下链接以了解更多信息:https://dev59.com/4XRB5IYBdhLWcg3w1Kde#499594和https://blog.httpwatch.com/2009/02/20/how-secure-are-query-strings-over-https/ 。 - user484261
@user484261 这条评论被严重低估了。 - Jordan Breton

22

对于仍在2021年苦苦挣扎的人来说,Node JS全局网络套接字类在构造函数中有一个额外的options字段。如果您进入WebSockets类的实现,您将找到此变量声明。您可以看到它接受三个参数url,这是必需的,protocols(可选),它可以是字符串、字符串数组或null。然后是第三个参数,即options。我们感兴趣的是一个对象,仍然是可选的。详见...

declare var WebSocket: {
    prototype: WebSocket;
    new (
        uri: string,
        protocols?: string | string[] | null,
        options?: {
            headers: { [headerName: string]: string };
            [optionName: string]: any;
        } | null,
    ): WebSocket;
    readonly CLOSED: number;
    readonly CLOSING: number;
    readonly CONNECTING: number;
    readonly OPEN: number;
};

如果你正在使用Node Js库,如React、React-Native,这里是一个示例,展示如何完成相应操作。

 const ws = new WebSocket(WEB_SOCKETS_URL, null, {
    headers: {
      ['Set-Cookie']: cookie,
    },
  });

关于我传递了空值的协议,请注意。如果您正在使用jwt,可以通过 Bearer + token 传递授权标头。

免责声明,这可能不受所有浏览器的支持。从MDN网站文档中可以看到只有两个参数是记录在案的。 请参见https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#syntax


21
这是一个关于2023年疲惫的旅行者偶然发现的情况的快速总结,而且很可能在很长一段时间后仍然如此。
自从这个问题被提出以来,12年过去了,情况没有任何改变。所有浏览器供应商都放弃了JavaScript WebSocket API(尽管实现偶尔会得到更新),而新的规范(WebSocket Stream和WebTransport)还远未实现。这意味着WebSockets仍然被广泛使用,尽管已经有7年之久被称为“遗留”,但并没有替代这个破损API的解决方案存在,而且问题的概述仍然令人讨厌,甚至更加恼人。
处理这种情况的选择(剧透,你可能想要选择第5或第6个选项):
1. 在外部实现身份验证
描述在https://devcenter.heroku.com/articles/websocket-security中。客户端需要向专用的终端点发出经过身份验证的请求,该终端点将生成并持久化一个短期令牌,该令牌也将发送给客户端。然后,客户端在打开WebSocket时将此令牌作为URL参数返回。服务器可以验证它并接受/拒绝协议升级。这要求服务器为WebSocket实现一个完全定制且有状态的身份验证机制,这在许多情况下都是一个过于复杂的桥梁。

2. 通过WebSocket发送身份验证信息

您在未经身份验证的情况下打开WebSocket,然后在执行其他任何操作之前通过WebSocket发送您的身份验证信息。从理论上讲,这听起来是合乎逻辑的(并且是浏览器供应商建议的),但只要稍加思考就会发现其中的问题。服务器需要实现一个笨拙、高度有状态且完全定制的身份验证机制,这与其他任何东西都不兼容,而且还需要与拒绝进行身份验证的客户端保持持久连接,这为拒绝服务攻击留下了一个大门,或者需要进行严格的超时限制以防止恶意行为。

3. 通过URL参数发送认证信息(例如访问令牌)
听起来并不可怕,只要强制使用SSL(`wss://`而不是`ws://`),因为WebSocket的URL是特殊的,不会保存在浏览器历史记录或类似的地方。此外,访问令牌通常具有较短的生命周期,这也减轻了危险。但是,服务器很可能会在某个时候记录URL。即使您的服务器应用程序没有记录,框架或(云)主机可能会记录。此外,如果您必须传递ID令牌(例如Firebase),您可能会因为ID令牌过大而遇到各种URL长度限制。
4. 通过传统的cookie进行认证
不要这样做。WebSockets不受同源策略限制(因为显然WebSockets的每一个细节都必须令人讨厌),允许使用cookie会让您完全暴露于XSRF攻击。修复这个问题比从这个列表中采取其他任何方法都更困难,因此根本不值得考虑。
5. 在Sec-WebSocket-Protocol中走私访问令牌
由于浏览器只允许您控制一个标题,即Sec-WebSocket-Protocol,您可以滥用它来模拟任何其他标题。有趣的是(或者可以说是滑稽的),这就是Kubernetes正在做的事情。简而言之,您将所需的身份验证附加为Sec-WebSocket-Protocol中的额外支持的子协议:
var ws = new WebSocket("ws://example.com/path", ["realProtocol", "yourAccessTokenOrSimilar"]);

然后,在服务器上,您可以添加某种中间件,将请求转换回其更合理的形式,然后将其进一步传递到系统中。这种方法可能不太好,但目前是最好的解决方案。URL中没有令牌,除了一些小的中间件之外,也没有自定义身份验证,服务器上也不需要额外的状态。
6. 如果适用,切换到SSE(或RSocket)。
对于很多情况来说,SSE可能是一个不错的替代方案。浏览器的EventSource API(就像WebSocket一样,它只能发送GET请求,不能发送头部信息,尽管它是常规的HTTP),但是!它可以很容易地被fetch替代,这是一个更合理的API。这种方法在GraphQL订阅等场景中作为WebSocket的替代方案效果很好,或者在任何不需要全双工的地方都可以使用。而且这可能涵盖了大多数情况。理论上,RSocket也可以是一个选择,但是考虑到它在浏览器中是通过WebSocket实现的,我不认为它实际上解决了任何问题,但是我没有深入研究,无法绝对确定。

1
谢谢你的回答!这个答案帮我省了很多时间去做研究! - undefined
你介意提供关于如何为#5(走私)设置中间件的更多细节吗?具体来说,在Kubernetes集群中,有没有一种简单的方法为我的Nginx Ingress Controller设置某种中间件? - undefined
我明白了,问题在于配置片段(据我所知,这是你所建议的唯一方法)存在安全漏洞(请参阅https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md#configuration-snippet)。最终,我实现了一个WebSocket代理服务,使用Go语言提取并附加标头(请参阅https://stackoverflow.com/a/77547168/13752922)。不过,我相信还有更好的选择。 - undefined
1
顺便说一句,如果有人需要更简单的解决方案,可以使用auth-url与auth-response-headers结合起来实现(参考链接:https://medium.com/@ankit.wal/authenticate-requests-to-apps-on-kubernetes-using-nginx-ingress-and-an-authservice-37bf189670ee)。具体来说,可以使用auth-url来访问一个服务,该服务提取相关的头信息,然后将该头信息返回给请求的响应。然后使用auth-response-headers将该头信息应用到Ingress的请求中。我本来想使用这种方法,但我的目标与会话亲和性有关,而这种方法不起作用。 - undefined
@Alita 是的,我稍微了解了一下,确实看起来是最好的选择。 - undefined
显示剩余2条评论

6
完全按照这种方法进行黑客攻击,感谢kanaka的答案。
客户端:
var ws = new WebSocket(
    'ws://localhost:8080/connect/' + this.state.room.id, 
    store('token') || cookie('token') 
);

服务器 (在这个例子中使用Koa2,但无论在哪里都应该类似):

var url = ctx.websocket.upgradeReq.url; // can use to get url/query params
var authToken = ctx.websocket.upgradeReq.headers['sec-websocket-protocol'];
// Can then decode the auth token and do any session/user stuff...

7
这是否只是将您的令牌传递到客户端应请求一个或多个特定协议的部分?我也可以让这个工作,没有问题,但我决定不这样做,而是像Motes建议的那样阻塞直到在onOpen()上发送了认证令牌。对我来说,过载协议请求头似乎是错误的,并且由于我的API面向公众使用,我认为这对API的使用者来说可能会有点困惑。 - Jay

5

在我的情况下(Azure 时间序列洞察 wss://)

使用 ReconnectingWebsocket 包装器,通过简单的解决方案能够实现添加标头:

socket.onopen = function(e) {
    socket.send(payload);
};

在这种情况下,payload是指:

{
  "headers": {
    "Authorization": "Bearer TOKEN",
    "x-ms-client-request-id": "CLIENT_ID"
}, 
"content": {
  "searchSpan": {
    "from": "UTCDATETIME",
    "to": "UTCDATETIME"
  },
"top": {
  "sort": [
    {
      "input": {"builtInProperty": "$ts"},
      "order": "Asc"
    }], 
"count": 1000
}}}

在他们的API中没有看到传递自定义标头的通用方式,这似乎不是一种通用方式。它是一种通用方式还是仅适用于Azure和某些服务器端配置? - Naga Kiran
这可能对某些人来说是一个不错的解决方法,但值得注意的是,这将为特别喜欢聊天的用例增加大量IO(每个请求都会发送头文件)。 - Michael Fry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接