我有两个aspnet.core服务。一个用于IdentityServer 4,另一个用于Angular4 +客户端使用的API。 SignalR集线器在API上运行。整个解决方案在docker上运行,但这无关紧要(见下文)。
我使用隐式身份验证流程,它可以完美运行。 NG应用程序重定向到用户登录的IdentityServer的登录页面。之后,浏览器将被重定向回带有访问令牌的NG应用程序。然后,令牌用于调用API并与SignalR建立通信。我想我已经阅读了所有可用的内容(参见下面的资料)。
由于SignalR使用不支持头的websockets,因此令牌应该在查询字符串中发送。然后在API端,提取令牌并为请求设置令牌,就像它在标题中一样。然后验证令牌并授权用户。
API在没有任何问题的情况下工作,用户获得授权,并且可以在API端检索声明。因此,IdentityServer应该没有问题,因为SignalR不需要任何特殊配置。我是对的吗?
当我不使用SignalR集线器上的[授权]属性时,握手成功。这就是为什么我认为我使用的docker基础结构和反向代理没有任何问题(代理设置为启用websockets)。
因此,未经授权,SignalR可以正常工作。通过授权,NG客户端在握手期间获得以下响应:
Failed to load resource: the server responded with a status of 401
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
请求是
Request URL: https://publicapi.localhost/context/negotiate?signalr_token=eyJhbGciOiJSUz... (token is truncated for simplicity)
Request Method: POST
Status Code: 401
Remote Address: 127.0.0.1:443
Referrer Policy: no-referrer-when-downgrade
我得到的回应:
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:4200
content-length: 0
date: Fri, 01 Jun 2018 09:00:41 GMT
server: nginx/1.13.10
status: 401
vary: Origin
www-authenticate: Bearer
根据日志,令牌已成功验证。我可以包含完整的日志,但我怀疑问题出在哪里。所以我将在这里包括那部分:
[09:00:41:0561 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
[09:00:41:0564 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
我在日志文件中得到这些,我不确定它是什么意思。我在我获得的API中包含代码部分,并提取令牌以及身份验证配置。
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.Authority = "http://identitysrv";
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
options.JwtBearerEvents.OnMessageReceived = context =>
{
if (context.Request.Query.TryGetValue("signalr_token", out StringValues token))
{
context.Options.Authority = "http://identitysrv";
context.Options.Audience = "publicAPI";
context.Token = token;
context.Options.Validate();
}
return Task.CompletedTask;
};
});
系统中没有其他错误,例外。我可以调试应用程序,一切似乎都没问题。
包含的日志行是什么意思? 如何调试授权期间发生的事情?
编辑:我差点忘了提到,我认为问题出在验证方案上,所以我将每个方案设置为我认为需要的方案。但遗憾的是它没有帮助。
我在这里很无能为力,所以我很感激任何建议。感谢。
信息来源:
Securing SignalR with IdentityServer
答案 0 :(得分:12)
我必须回答我自己的问题,因为我有一个截止日期,令人惊讶的是我设法解决了这个问题。所以我写下来希望它将来能帮助某人。
首先,我需要了解发生了什么,所以我将整个授权机制替换为我自己的。我可以通过这段代码来做到这一点。解决方案不需要它,但是如果有人需要它,这是可行的方法。
services.Configure<AuthenticationOptions>(options =>
{
var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
scheme.HandlerType = typeof(CustomAuthenticationHandler);
});
在IdentityServerAuthenticationHandler的帮助下并覆盖所有可能的方法,我终于明白在检查令牌后执行OnMessageRecieved事件。因此,如果在调用HandleAuthenticateAsync期间没有任何令牌,则响应将为401.这有助于我找出放置自定义代码的位置。
我需要在令牌检索期间实现自己的“协议”。所以我取代了那个机制。
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority = "http://identitysrv";
options.TokenRetriever = CustomTokenRetriever.FromHeaderAndQueryString;
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
});
重要的部分是TokenRetriever属性以及替换它的内容。
public class CustomTokenRetriever
{
internal const string TokenItemsKey = "idsrv4:tokenvalidation:token";
// custom token key change it to the one you use for sending the access_token to the server
// during websocket handshake
internal const string SignalRTokenKey = "signalr_token";
static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }
static CustomTokenRetriever()
{
AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
}
public static string FromHeaderAndQueryString(HttpRequest request)
{
var token = AuthHeaderTokenRetriever(request);
if (string.IsNullOrEmpty(token))
{
token = QueryStringTokenRetriever(request);
}
if (string.IsNullOrEmpty(token))
{
token = request.HttpContext.Items[TokenItemsKey] as string;
}
if (string.IsNullOrEmpty(token) && request.Query.TryGetValue(SignalRTokenKey, out StringValues extract))
{
token = extract.ToString();
}
return token;
}
这是我的自定义标记检索算法,它首先尝试标准标题和查询字符串,以支持Web API调用等常见情况。但是如果令牌仍然是空的,它会尝试从客户端在websocket握手期间放置它的查询字符串中获取它。
答案 1 :(得分:3)
我知道这是一个旧线程,但是如果有人像我一样偶然发现了这个线程。我找到了替代解决方案。
TLDR:JwtBearerEvents.OnMessageReceived,将在按如下所示使用它之前检查令牌,然后对其进行检查:
public void ConfigureServices(IServiceCollection services)
{
// Code removed for brevity
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://myauthority.io";
options.ApiName = "MyApi";
options.JwtBearerEvents = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/myhubname")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
此Microsoft文档给了我一个提示: https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1。但是,在Microsoft示例中,将调用options.Events,因为它不是使用IdentityServerAuthentication的示例。如果在Microsoft示例中使用options.JwtBearerEvents和options.Events的方式相同,则IdentityServer4很高兴!
答案 2 :(得分:2)
让我为此付两分钱。我认为我们大多数人都将令牌存储在cookie中,并且在WebSockets握手期间,令牌也将发送到服务器,因此我建议使用从cookie中检索令牌。
为此,请将其添加到最后一个if
语句下面:
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
token = cookieToken;
}
实际上,我们完全可以根据Microsoft docs从查询字符串中删除检索,这并不是真正安全的方法,可以记录在某处。