SignalR 安全性简介

作者 :Patrick FletcherTom FitzMacken

警告

本文档不适用于最新版本的 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 中心,但不应用于持久连接。 若要在使用 时强制实施授权规则,PersistentConnectionAuthorizeRequest必须重写 方法。 有关持久连接的详细信息,请参阅 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://”请求。

通常,使用 Cookie 进行身份验证的网站可能会受到 CSRF 攻击,因为浏览器会将所有相关 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 Forms应用程序或 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.");
    }
}