SignalR 安全性简介

作者 :帕特里克·弗莱彻汤姆·菲茨马克肯

警告

本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR

本文介绍开发 SignalR 应用程序时必须考虑的安全问题。

本主题中使用的软件版本

本主题的早期版本

有关 SignalR 早期版本的信息,请参阅 SignalR 旧版本

问题和评论

请留下有关你喜欢本教程的方式以及我们在页面底部的评论中可以改进的内容的反馈。 如果存在与本教程不直接相关的问题,可以将这些问题发布到 ASP.NET SignalR 论坛StackOverflow.com

概述

本文档包含以下各节:

SignalR 安全概念

身份验证和授权

SignalR 不提供用于对用户进行身份验证的任何功能。 而是将 SignalR 功能集成到应用程序的现有身份验证结构中。 像在应用程序中一样对用户进行身份验证,并在 SignalR 代码中处理身份验证结果。 例如,可以使用 ASP.NET 表单身份验证对用户进行身份验证,然后在中心强制实施哪些用户或角色有权调用方法。 在中心内,还可以将身份验证信息(例如用户名或用户是否属于角色)传递给客户端。

SignalR 提供 Authorize 属性来指定哪些用户有权访问中心或方法。 将 Authorize 属性应用于中心中的中心或特定方法。 如果没有 Authorize 属性,中心上的所有公共方法都可用于连接到中心的客户端。 有关中心的详细信息,请参阅 SignalR 中心的身份验证和授权

Authorize 属性应用于中心,但不能将持久连接应用到中心。 若要在使用 PersistentConnection 方法时强制实施授权规则,必须重写 AuthorizeRequest 该方法。 有关持久连接的详细信息,请参阅 SignalR 持久连接的身份验证和授权

连接令牌

SignalR 通过验证发送方的标识来缓解执行恶意命令的风险。 对于每个请求,客户端和服务器都会传递一个连接令牌,其中包含经过身份验证的用户的连接 ID 和用户名。 连接 ID 唯一标识每个连接的客户端。 创建新连接时,服务器会随机生成连接 ID,并在连接期间保留该 ID。 Web 应用程序的身份验证机制提供用户名。 SignalR 使用加密和数字签名来保护连接令牌。

此图显示了从“客户端新建连接请求”到“服务器接收的连接请求”到“客户端接收的响应”的箭头。身份验证系统会在“响应”和“接收的响应”框中生成连接令牌。

对于每个请求,服务器将验证令牌的内容,以确保请求来自指定的用户。 用户名必须与连接 ID 相对应。通过验证连接 ID 和用户名,SignalR 可防止恶意用户轻松模拟其他用户。 如果服务器无法验证连接令牌,请求将失败。

显示从客户端请求到服务器接收到已保存令牌的请求的箭头的关系图。连接令牌和消息位于“客户端”框和“服务器”框中。

由于连接 ID 是验证过程的一部分,因此不应向其他用户显示一个用户的连接 ID 或将值存储在客户端上,例如在 Cookie 中。

连接令牌与其他令牌类型

连接令牌偶尔被安全工具标记,因为它们似乎是会话令牌或身份验证令牌,如果公开,这会带来风险。

SignalR 的连接令牌不是身份验证令牌。 它用于确认发出此请求的用户与创建连接的用户相同。 连接令牌是必需的,因为 ASP.NET SignalR 允许连接在服务器之间移动。 该令牌将连接与特定用户相关联,但不断言发出请求的用户的标识。 若要正确验证 SignalR 请求,必须具有一些其他令牌来断言用户的标识,例如 Cookie 或持有者令牌。 但是,连接令牌本身不声明请求是由该用户发出的,只有令牌中包含的连接 ID 与该用户相关联。

由于连接令牌不提供自己的身份验证声明,因此它不被视为“会话”或“身份验证”令牌。 获取给定用户的连接令牌并在作为其他用户(或未经身份验证的请求)进行身份验证的请求中重播该令牌将失败,因为请求的用户标识和存储在令牌中的标识不匹配。

重新连接时重新加入组

默认情况下,SignalR 应用程序会在从临时中断重新连接时自动将用户重新分配给相应的组,例如在连接超时之前删除并重新建立连接。重新连接时,客户端会传递一个组令牌,其中包含连接 ID 和分配的组。 组令牌经过数字签名和加密。 重新连接后,客户端保留相同的连接 ID;因此,从重新连接客户端传递的连接 ID 必须与客户端使用的上一个连接 ID 匹配。 此验证可防止恶意用户在重新连接时传递请求以加入未经授权的组。

但是,请务必注意,组令牌不会过期。 如果用户过去属于某个组,但被禁止该组,则该用户可能能够模拟包含被禁止组的组令牌。 如果需要安全地管理哪些用户属于哪些组,则需要将该数据存储在服务器上,例如在数据库中。 然后,将逻辑添加到应用程序中,以验证用户是否属于组。 有关验证组成员身份的示例,请参阅 “使用组”。

仅当连接在临时中断后重新连接时,才自动重新加入组。 如果用户通过导航离开应用程序或应用程序重启断开连接,应用程序必须处理如何将该用户添加到正确的组。 有关详细信息,请参阅 “使用组”。

SignalR 如何防止跨站点请求伪造

跨站点请求伪造(CSRF)是一种攻击,恶意站点向用户当前登录的易受攻击站点发送请求。 SignalR 通过使恶意站点极不可能为 SignalR 应用程序创建有效请求来防止 CSRF。

CSRF 攻击的说明

下面是 CSRF 攻击的示例:

  1. 用户使用表单身份验证登录 www.example.com

  2. 服务器对用户进行身份验证。 来自服务器的响应包括身份验证 Cookie。

  3. 如果不注销,用户将访问恶意网站。 此恶意网站包含以下 HTML 表单:

    <h1>You Are a Winner!</h1>
    <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click Me"/>
    </form>
    

    请注意,表单作发布到易受攻击的站点,而不是恶意站点。 这是 CSRF 的“跨网站”部分。

  4. 用户单击“提交”按钮。 浏览器包含请求的身份验证 Cookie。

  5. 请求在具有用户身份验证上下文的 example.com 服务器上运行,并且可以执行经过身份验证的用户允许执行的任何作。

尽管此示例要求用户单击窗体按钮,但恶意页面可能同样轻松地运行将 AJAX 请求发送到 SignalR 应用程序的脚本。 此外,使用 SSL 不会阻止 CSRF 攻击,因为恶意站点可以发送“https://”请求。

通常,CSRF 攻击可能会针对使用 Cookie 进行身份验证的网站,因为浏览器会将所有相关 Cookie 发送到目标网站。 但是,CSRF 攻击不限于利用 Cookie。 例如,基本身份验证和摘要式身份验证也容易受到攻击。 用户使用基本身份验证或摘要式身份验证登录后,浏览器会自动发送凭据,直到会话结束。

SignalR 采取的 CSRF 缓解措施

SignalR 采取以下步骤,防止恶意站点向应用程序创建有效的请求。 默认情况下,SignalR 会执行这些步骤,无需在代码中执行任何作。

  • 禁用跨域请求 SignalR 禁用跨域请求,以防止用户从外部域调用 SignalR 终结点。 SignalR 将来自外部域的任何请求视为无效并阻止请求。 建议保留此默认行为;否则,恶意站点可能会欺骗用户向站点发送命令。 如果需要使用跨域请求,请参阅 如何建立跨域连接
  • 在查询字符串中传递连接令牌,而不是 Cookie SignalR 将连接令牌作为查询字符串值而不是 Cookie 传递。 将连接令牌存储在 Cookie 中不安全,因为浏览器在遇到恶意代码时可能会无意中转发连接令牌。 此外,在查询字符串中传递连接令牌可防止连接令牌在当前连接之外保留。 因此,恶意用户无法在其他用户的身份验证凭据下发出请求。
  • 验证连接令牌“连接令牌 ”部分所述,服务器知道哪个连接 ID 与每个经过身份验证的用户相关联。 服务器不会处理来自与用户名不匹配的连接 ID 的任何请求。 恶意用户不太可能猜测有效的请求,因为恶意用户必须知道用户名和当前随机生成的连接 ID。连接 ID 在连接结束后立即变为无效。 匿名用户不应有权访问任何敏感信息。

SignalR 安全建议

安全套接字层 (SSL) 协议

SSL 协议使用加密来保护客户端和服务器之间的数据传输。 如果 SignalR 应用程序在客户端和服务器之间传输敏感信息,请使用 SSL 进行传输。 有关设置 SSL 的详细信息,请参阅 如何在 IIS 7 上设置 SSL

不要将组用作安全机制

组是收集相关用户的便捷方法,但它们不是限制对敏感信息的访问的安全机制。 当用户可以在重新连接期间自动重新加入组时,这尤其如此。 相反,请考虑将特权用户添加到角色,并将对中心方法的访问权限限制为仅该角色的成员。 有关基于角色限制访问的示例,请参阅 SignalR 中心的身份验证和授权。 有关在重新连接时检查用户对组的访问权限的示例,请参阅 “使用组”。

安全地处理来自客户端的输入

若要确保恶意用户不向其他用户发送脚本,必须对用于广播到其他客户端的客户端的所有输入进行编码。 应在接收客户端而不是服务器上对消息进行编码,因为 SignalR 应用程序可能有许多不同类型的客户端。 因此,HTML 编码适用于 Web 客户端,但不适用于其他类型的客户端。 例如,用于显示聊天消息的 Web 客户端方法将通过调用 html() 函数安全地处理用户名和消息。

chat.client.addMessageToPage = function (name, message) {
    // Html encode display name and message. 
    var encodedName = $('<div />').text(name).html();
    var encodedMsg = $('<div />').text(message).html();
    // Add the message to the page. 
    $('#discussion').append('<li><strong>' + encodedName
        + '</strong>:  ' + encodedMsg + '</li>');
};

将用户状态更改与活动连接相协调

如果用户的身份验证状态在活动连接存在时发生更改,用户将收到一条错误,指出“用户标识在活动 SignalR 连接期间无法更改”。在这种情况下,应用程序应重新连接到服务器,以确保连接 ID 和用户名协调。 例如,如果应用程序允许用户在活动连接存在时注销,则连接的用户名将不再与下一个请求传入的名称匹配。 在用户注销之前,需要停止连接,然后重新启动它。

但是,请务必注意,大多数应用程序不需要手动停止和启动连接。 如果应用程序在注销后将用户重定向到单独的页面,例如 Web 窗体应用程序或 MVC 应用程序中的默认行为,或者在注销后刷新当前页,则活动连接会自动断开连接,不需要任何其他作。

以下示例演示如何在用户状态发生更改时停止和启动连接。

<script type="text/javascript">
    $(function () {
        var chat = $.connection.sampleHub;
        $.connection.hub.start().done(function () {
            $('#logoutbutton').click(function () {
                chat.connection.stop();
                $.ajax({
                    url: "Services/SampleWebService.svc/LogOut",
                    type: "POST"
                }).done(function () {
                    chat.connection.start();
                });
            });
        });
    });
</script>

或者,如果你的网站使用表单身份验证的滑动过期,并且没有活动来保持身份验证 Cookie 有效,则用户的身份验证状态可能会更改。 在这种情况下,用户将注销,用户名将不再与连接令牌中的用户名匹配。 可以通过添加一些脚本来定期请求 Web 服务器上的资源以保持身份验证 Cookie 有效来解决此问题。 以下示例演示如何每 30 分钟请求一次资源。

$(function () {
    setInterval(function() {
        $.ajax({
            url: "Ping.aspx",
            cache: false
        });
    }, 1800000);
});

自动生成的 JavaScript 代理文件

如果不想在每个用户的 JavaScript 代理文件中包括所有中心和方法,则可以禁用文件的自动生成。 如果你有多个中心和方法,但不希望每个用户知道所有方法,则可以选择此选项。 通过将 EnableJavaScriptProxies 设置为 false 来禁用自动生成。

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
app.MapSignalR(hubConfiguration);

有关 JavaScript 代理文件的详细信息,请参阅 生成的代理及其用途

例外

应避免将异常对象传递给客户端,因为这些对象可能会向客户端公开敏感信息。 而是在客户端上调用显示相关错误消息的方法。

public Task SampleMethod()
{
    try
    { 
        // code that can throw an exception
    }
    catch(Exception e)
    {
        // add code to log exception and take remedial steps

        return Clients.Caller.DisplayError("Sorry, the request could not be processed.");
    }
}