我正在使用基于令牌的授权制作基于signalr的服务。令牌由外部API验证,该API返回用户的ID。然后,将通知发送给具有特定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(向用户发送通知"1"(每2秒):
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;
}
}
调试。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 + 'nn' + 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的服务器)
我在这里错过了什么?
它看起来不像你正在处理来自查询字符串的验证令牌,这在某些情况下是必需的,例如来自浏览器的WebSocket连接。
有关如何处理持票人认证的一些信息,请参阅https://learn.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#built-in-jwt-authentication。
问题解决。正如@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();