事件源(服务器发送事件)中的HTTP授权标头

100

我需要为HTML5 EventSource设置一个授权头部。由于Websockets出现后,Server Sent Events似乎已经不再使用,因此我无法找到任何有用的文档。我已经找到的方法是在url中传递授权数据...但我不喜欢这种方法。

我正在使用AngularJS并在$httpProvider上设置拦截器,但EventSource没有被AngularJS拦截,因此我无法添加任何头部。


请问您能分享一下您目前已经有的代码吗? - tim
你用更好的方法修复了这个问题吗?@Alvaro Luis Bustamante - Dolphin
10个回答

36

我知道您的帖子已经过去一年了,但是我发现自己也面临同样的问题,却找不到好的答案。我希望这可以帮助某些人,或者至少给他们一些想法...

Cookie看起来很容易,但如果有人阻止Cookie怎么办?我必须提示他们启用Cookie才能使用该网站。此时,他们开始怀疑是否可以信任该网站,因为他们出于“安全原因”禁用了Cookie。然而,与此同时,我希望出于安全原因启用Cookie!

使用AJAX,可以轻松地通过SSL POST身份验证数据,但对于SSE来说,这是不可能的。我看过许多帖子,人们会说,“只需使用查询字符串”,但我不想通过明文发送auth数据(例如example.com/stream?sessionID=idvalue)来危及客户的安全,因为有人可能会偷窥。

经过几个小时的思考,我意识到我可以在不危及客户auth数据的情况下实现整体目标。需要澄清的是,我没有发现在建立EventSource连接时进行POST的方法,但它确实允许浏览器每次重新连接时与EventSource一起安全地传递身份验证令牌。关键是将所需的sessionID / token传递到lastEventID中。

用户可以像往常一样使用用户名/密码进行身份验证(或通过AJAX POST在localstorage中保存的令牌进行身份验证)。 AJAX身份验证过程将返回一个JSON对象,其中包含一个短期令牌(60秒后过期或使用时过期),该令牌将与更持久的令牌一起保存在您选择的后端(例如:mySQL)中。此时,您可以启动SSE连接,如:

    qString = "?slt=" + "value-that-expires-within-seconds";
    streamURL = "http://example.com/stream.php";
    var streamSource = new EventSource(streamURL + qString);

    streamSource.addEventListener('auth',function(e) {
        var authStatus = JSON.parse(e.data);
        if (authStatus.session !== 'valid') {
            qString = "";
            streamSource.close();
        }
    })

在相应的PHP代码中,你需要像这样做:
        header("Content-Type: text/event-stream\n");
        ob_end_flush();
        ob_start();

        if (isThisShortLivedTokenValid($_GET["slt"])) {
            // The short-lived-token is still valid... so we will lookup
            // the value of the corresponding longer-lasting token and
            // IMMEDIATELY invalidate the short-lived-token in the db.
            sendMsg($realToken,'auth','session','valid');
            exit;
        } else if (isThisRealTokenValid($_SERVER["HTTP_LAST_EVENT_ID"])){
            while (1) {
                // normal code goes here
                // if ($someCondition == 'newDataAvailable') sendMsg($realToken,'chat','msg-id','msg-content');
            }
        } else {
            http_response_code(404); // stop the browser from reconnecting.
            exit; //quit the PHP script and don't send anything.
        }


        function sendMsg($id, $event, $key, $val) {
            echo "{" . PHP_EOL;
            echo "event: " . $event . PHP_EOL;
            echo "id: $id" . PHP_EOL;
            echo 'data: {"' . $key . '" : "' . $val . '"}' . PHP_EOL;
            echo "}" . PHP_EOL;
            echo PHP_EOL;
            ob_flush();
            flush();
        }

        function isThisShortLivedTokenValid($sltValue) {
            //stuff to connect to DB and determine if the
            //value is still valid for authentication
            return $dbResult == $sltValue ? TRUE : FALSE;
        }

SSE连接到短暂的令牌,PHP验证该令牌并从数据库中删除,因此它将永远不能再进行身份验证。这有点类似于收到一个六位数代码以登录在线银行。我们使用PHP推送从数据库检索到的真正过期时间较晚的令牌作为事件ID。JavaScript实际上不需要对此事件进行任何操作 - 服务器将自动结束连接,但如果您想要更多功能,则可以监听该事件。
此时,SSE连接已结束,因为PHP完成了脚本。然而,浏览器将自动重新建立连接(通常需要3秒)。这次,它将发送lastEventId ... 我们在断开连接之前将其设置为令牌值。在下一次连接时,此值将用作我们的令牌,应用程序将按预期运行。只要开始使用真正的令牌作为事件ID发送消息/事件,就不必断开连接。此令牌值在SSL上完全加密传输,当浏览器接收到它时以及每个后续连接到服务器时都是如此。明文传输的值在我们接收并使用它后几秒钟内即已过期,任何发现它的人都无法再使用它。如果有人尝试使用它,他们将收到404 RESPONSE。
如果您已经将事件流ID用于其他目的,则可能无法“开箱即用”,除非您将授权令牌和先前使用的值连接起来并将其拆分为变量,以便对于应用程序的其余部分是透明的。类似如下内容:
    // when sending data, send both values
    $sseID = $token_value . "_" . $previouslyUsedID;
    sendMsg($sseID,'chat','msg-id','msg-content');

    // when a new connection is established, break apart the values
    $manyIDs = explode("_", $_SERVER["HTTP_LAST_EVENT_ID"])
    $token_value = $manyIDs[0]
    $previouslyUsedID = $manyIDs[1]

4
看起来你甚至不需要完成其中的大部分工作——让客户端发送一个普通的POST请求,例如/getAuthCodeForEventConnection,并附上其授权凭据。服务器会在POST响应中返回一个短暂的令牌,客户端立即将其与新的EventSource(${uri}?slt=${sltToken})一起发送。然后服务器建立自己的用户和特定套接字连接之间的关联。似乎不需要断开连接或使用某种奇怪的上次接收到的id字段的排列方式。 - Eric Blade
6
FYI查询字符串在https下是安全的,你不希望使用它们的原因是它们经常被记录在日志记录器中。参见https://dev59.com/I3RC5IYBdhLWcg3wW_tk - thinktt
我已经在WHATWG问题中链接了您的问题和答案:https://github.com/whatwg/html/issues/2177 - 希望这能在未来有所帮助。 - Tobias Münch

23

EventSource没有发送HTTP头到服务器的API。当我使用SSE构建实时聊天时,我也曾为此问题苦恼。

然而,如果您的SSE服务器与身份验证服务器相同,则认为cookie将自动发送。


20

window.EventSource目前似乎不支持传递额外的标头。好消息是还有一些其他流行的EventSource实现支持附加标头。以下是其中一些:

const eventSource = new EventSource(resoureUrl, {
            headers: {
                'Authorization': 'Bearer ' + authorizationToken
            }
        });

eventSource.onmessage = result => {
    const data = JSON.parse(result.data);
    console.log('Data: ', data);
};

eventSource.onerror = err => {
    console.log('EventSource error: ', err);
};

1
我想不出来:这是完全替代ServerSideEvents SignalR Hub连接的吗? - Alexander
似乎事件源不支持头部信息。 - Dolphin

18

这个Polyfill添加了授权头支持:https://github.com/Yaffle/EventSource/

因此,您可以执行以下操作:

new EventSource("https://domain/stream", { authorizationHeader: "Bearer ..." });

1
你能提供一个与此相关的 Angular 代码片段吗?我无法实现它 :l - pihh-rocks
1
这个 polyfill 不支持用户定义的标头。也许你想链接到一个分支吗? - AlexG
5
由于原生EventSource不支持用户定义的头部,所以它已经不是一个polyfill了,因此当您的代码使用原生EventSource时,您将会得到一个隐藏的错误/bug。 - maksimr
21
关于这个实现的说明:阅读了代码后,发现它使用定时器来包装XMLHttpRequest以实现类似EventSource的结果。这不是批评,只是在考虑服务器连接负载时需要考虑的一个要点。 - Scott D. Strader

12

自从这个问题被问出来以后,EventSource领域发生了很多事情。

所有主要的浏览器现在支持获取流。

这意味着你现在可以在这些流之上实现一个EventSource
这似乎是为什么头部从未添加到EventSource规范中的主要原因

一个基于fetch的EventSource实现的例子是https://github.com/Azure/fetch-event-sourcenpm install @microsoft/fetch-event-source)。


太好用了!还是微软的套装! - Tom McDonough
但只有EventSource在浏览器的网络页面上显示数据。任何自定义实现都不行... :( - undefined
就连那个图书馆似乎也没有得到支持。为什么我们不能拥有好东西呢? - undefined

5
如果您使用这个事件源polyfill的分支,您可以像rafaelzlisboa描述的那样添加授权标头:https://github.com/AlexGalays/EventSource#923b9a0998fcfd7753040e09aa83764b3cc0230d.
我不知道您是否可以像rafaelzlisboa的示例中一样提供第二个参数作为认证标头。我通过创建一个头对象,并将我的授权标头放在其中来使其工作,例如: new EventSource("https://domain/stream", { headers: { Authorization: Bearer.... }});

4

另一种传递身份验证令牌的方式是通过URL作为查询参数,但您应该考虑安全性。此外,要在服务器端添加支持通过查询参数进行授权的功能。


在这种情况下,可以使用SSL/TLS来确保令牌的安全。否则,在我看来,这个想法是最实用的。 - crollywood
6
@crollywood令牌并不是“安全可靠”的,因为它可能会被保存在Web服务器日志中的请求中(或者在客户端到服务器间的可能多个代理之一中)。 - Paolo

2

我通过在SSE调用之前增加额外的休息时间来解决了这个问题,这个休息时间是普通的休息时间,需要与我们在SSE调用中需要的安全协议相同,并且在响应中获取OTP。并将此OTP发送到sse调用查询参数中,在Web过滤器中验证此OTP并将其替换为身份验证标头。


我也教过这种方法,但是当多个后端与负载均衡一起使用时,它并不适用。但在许多情况下,它仍然是一个有效的选择。 - Lubo

2
正如其他人指出的那样,当前流行浏览器的原生EventSource实现不支持Authorization头部,而且很可能永远不会支持。此外,EventSource并非Angular HTTPClient,而是一个内置设施,所以即使假设它支持任何HTTP头部,Angular拦截器也无法与其配合使用。
更现代化的Fetch API提供了流式传输,这比旧的SSE更通用,并且也不受XMLHttpRequest的限制。
如果您需要坚持使用SSE(服务器端),那么Fetch Event Source package是目前最好的选择,因为它基于Fetch API,这比原始的EventSource API更符合时代潮流,而且在最近几年中,Chrome开发者决定维护Fetch API。
Fetch Event Source不包装XMLHttpRequest,而是使用来自Fetch API的流,因此它不会带来众所周知的XMLHttpRequest性能折衷。
它允许您添加任何需要的HTTP头,包括授权头。
await fetchEventSource('/api/sse', {
    headers: {
        'Authorization': 'Bearer my_token', // and/or any other headers you need
    },
    onmessage(ev) {
        console.log(ev.data);
    }
});

也许可以为此编写一个拦截器,但是我在使用时没有用到,因为我只需要在代码的几个地方使用它。

EventSource的语义与XMLHttpRequest完全不同。它们在底层完全不同,因此尽管两者的包装器可能具有类似的调用签名,但它们在根本上工作方式不同,并具有不同的性能特征。寻求实现SSE的人有其原因,并不打算使用XMLHttpRequest作为解决方案。 - Daniel Kats
1
请注意,我发布的EventSource包装器并没有包装XMLHttpRequest,而是使用来自Fetch API的流,因此它不会受到XMLHttpRequest包装器的已知性能问题的影响。我已相应地更新了我的答案。 - Lucio Crusca

0

我浏览了很多帖子,试图找出在EventSource()调用中是否可以发送auth token。虽然有一些polyfill替代方案允许添加头文件:https://github.com/whatwg/html/issues/2177,但其他人则提到通过ssl发送auth token。

与其使用polyfill EventSource()或通过ssl在查询参数中发送auth token,不如在EventSource url的参数中发送eventSource标识符(eventSrcUUID):

在用户认证时,服务器会生成eventSrcUUID以及sseEmitter,并将它们放置在sseEmitterMap中。

客户端从响应中检索eventSrcUUID,并使用该参数调用EventSource()。在服务器上,引用sseEmitterMap以检索eventSrc对象。保存在会话数据中的sseEmitter对象用于向客户端发送事件通知。


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