【问题标题】:SignalR + token authorization via a remote serviceSignalR + 通过远程服务进行令牌授权
【发布时间】:2021-12-11 03:32:58
【问题描述】:

我正在使用基于令牌的授权制作基于 SignalR 的服务。令牌由返回用户 ID 的外部 API 验证。然后,通知会发送给具有特定 ID 的用户。

我无法解决的问题是 SignalR 的客户端代码显然发送了 2 个请求:一个没有令牌(身份验证失败),另一个有令牌(身份验证成功)。由于某种原因,第一个结果被缓存,用户没有收到任何通知。

如果我对检查进行注释并始终返回正确的 ID,即使没有指定令牌,代码也会突然开始工作。

HubTOkenAuthenticationHandler.cs:

public class HubTokenAuthenticationHandler : AuthenticationHandler<HubTokenAuthenticationOptions>
{
    public HubTokenAuthenticationHandler(
        IOptionsMonitor<HubTokenAuthenticationOptions> options,
        ILoggerFactory logFactory,
        UrlEncoder encoder,
        ISystemClock clock,
        IAuthApiClient api
    )
        : base(options, logFactory, encoder, clock)
    {
        _api = api;
    }

    private readonly IAuthApiClient _api;

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        try
        {
            // uncommenting this line makes everything suddenly work
            // return SuccessResult(1);
            
            var token = GetToken();
            if (string.IsNullOrEmpty(token))
                return AuthenticateResult.NoResult();
        
            var userId = await _api.GetUserIdAsync(token); // always returns 1
            return SuccessResult(userId);
        }
        catch (Exception ex)
        {
            return AuthenticateResult.Fail(ex);
        }
    }
    
    /// <summary>
    /// Returns an identity with the specified user id.
    /// </summary>
    private AuthenticateResult SuccessResult(int userId)
    {
        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim(ClaimTypes.Name, userId.ToString())
            }
        );
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }

    /// <summary>
    /// Checks if there is a token specified.
    /// </summary>
    private string GetToken()
    {
        const string Scheme = "Bearer ";

        var auth = Context.Request.Headers["Authorization"].ToString() ?? "";
        return auth.StartsWith(Scheme)
            ? auth.Substring(Scheme.Length)
            : "";
    }
}

Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<FakeNotificationService>();
        services.AddSingleton<IAuthApiClient, FakeAuthApiClient>();
        
        services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

        services.AddAuthentication(opts =>
                {
                    opts.DefaultAuthenticateScheme = HubTokenAuthenticationDefaults.AuthenticationScheme;
                    opts.DefaultChallengeScheme = HubTokenAuthenticationDefaults.AuthenticationScheme;
                })
                .AddHubTokenAuthenticationScheme();

        services.AddRouting(opts =>
        {
            opts.AppendTrailingSlash = false;
            opts.LowercaseUrls = false;
        });

        services.AddSignalR(opts => opts.EnableDetailedErrors = true);
        services.AddControllers();
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseDeveloperExceptionPage();

        app.UseRouting();

        app.UseAuthentication();

        app.UseEndpoints(x =>
        {
            x.MapHub<InfoHub>("/signalr/info");
            x.MapControllers();
        });
    }
}

FakeNotificationsService.cs(每 2 秒向用户“1”发送一次通知):

public class FakeNotificationService: IHostedService
{
    public FakeNotificationService(IHubContext<InfoHub> hubContext, ILogger<FakeNotificationService> logger)
    {
        _hubContext = hubContext;
        _logger = logger;
        _cts = new CancellationTokenSource();
    }

    private readonly IHubContext<InfoHub> _hubContext;
    private readonly ILogger _logger;
    private readonly CancellationTokenSource _cts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // run in the background
        Task.Run(async () =>
        {
            var id = 1;
            while (!_cts.Token.IsCancellationRequested)
            {
                await Task.Delay(2000);
                await _hubContext.Clients.Users(new[] {"1"})
                                 .SendAsync("NewNotification", new {Id = id, Date = DateTime.Now});
                
                _logger.LogInformation("Sent notification " + id);

                id++;
            }
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        return Task.CompletedTask;
    }
}

Debug.cshtml(客户端代码):

<html>
<head>
    <title>SignalRPipe Debug Page</title>
</head>
<body>
    <h3>Notifications log</h3>
    <textarea id="log" cols="180" rows="40"></textarea>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/5.0.11/signalr.min.js"
            integrity="sha512-LGhr8/QqE/4Ci4RqXolIPC+H9T0OSY2kWK2IkqVXfijt4aaNiI8/APVgji3XWCLbE5J0wgSg3x23LieFHVK62g=="
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script language="javascript">
        var token = "123";

        var conn = new signalR
            .HubConnectionBuilder()
            .withUrl('/signalr/info', { accessTokenFactory: () => token })
            .configureLogging(signalR.LogLevel.Debug)
            .build();

        var logElem = document.getElementById('log');
        var id = 1;

        function log(text) {
            logElem.innerHTML = text + '\n\n' + logElem.innerHTML;
        }

        conn.on("NewNotification", alarm => {
            log(`[Notification ${id}]:\n${JSON.stringify(alarm)}`);
            id++;
        });

        conn.start()
            .then(() => log('Connection established.'))
            .catch(err => log(`Connection failed:\n${err.toString()}`));
    </script>
</body>
</html>

作为可运行项目的最小复制: https://github.com/impworks/signalr-auth-problem

我尝试了以下方法,但没有成功:

  • 添加一个允许一切的虚假授权处理程序
  • 将调试视图提取到单独的项目(基于 express.js 的服务器)

我在这里错过了什么?

【问题讨论】:

    标签: asp.net-core authentication signalr


    【解决方案1】:

    您似乎没有处理来自查询字符串的身份验证令牌,这在某些情况下是必需的,例如来自浏览器的 WebSocket 连接。

    请参阅https://docs.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#built-in-jwt-authentication,了解有关如何处理不记名身份验证的一些信息。

    【讨论】:

    • 我见过JwtBearerEvents sn-p,但我没有使用 JWT - 它仍然相关吗?
    • 但是,您似乎已经接近事实:将Transport 设置为LongPolling 可以解决此问题。我会尝试使用 JWT 设置
    【解决方案2】:

    问题已解决。正如@Brennan 正确猜测的那样,WebSockets 不支持标头,因此令牌是通过查询字符串传递的。我们只需要一点代码就可以从任一来源获取令牌:

    private string GetHeaderToken()
    {
        const string Scheme = "Bearer ";
    
        var auth = Context.Request.Headers["Authorization"].ToString() ?? "";
        return auth.StartsWith(Scheme)
            ? auth.Substring(Scheme.Length)
            : null;
    }
    
    private string GetQueryToken()
    {
        return Context.Request.Query["access_token"];
    }
    

    然后,在HandleAuthenticateAsync

    var token = GetHeaderToken() ?? GetQueryToken();
    

    【讨论】:

      猜你喜欢
      • 2013-04-13
      • 2014-03-17
      • 2014-06-30
      • 1970-01-01
      • 2021-09-04
      • 2019-07-16
      • 2015-04-24
      • 2018-06-13
      • 2020-12-22
      相关资源
      最近更新 更多