SignalR 授权在 asp.net 核心 angular 带有身份服务器的 SPA 中开箱即用
SignalR authorization not working out of the box in asp.net core angular SPA with Identity Server
请注意 - 这仅适用于服务器端身份的情况(即 IdentityServer4 创建令牌,而不是 Angular)
创建了全新的 asp.net core 5 angular spa 应用表单模板:
dotnet new angular --auth Individual
npm i @microsoft/signalr
已修改Startup.cs
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder
.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddSignalR();
. . .
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
. . .
endpoints.MapHub<NewsHub>("/newshub");
});
已添加中心 class
[Authorize]
public class NewsHub : Hub
{
}
修改后的 WeatherForecastController:
private IHubContext<NewsHub> _hub;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
{
_hub = hub;
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var timerManager = new TimerManager(() =>
_hub.Clients.All.SendAsync("servermessage", DateTime.Now.Ticks.ToString()));
修改fetch-data.component.ts
constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
http.get<WeatherForecast[]>(baseUrl + 'weatherforecast').subscribe(result => {
this.forecasts = result;
this.hub = new HubConnectionBuilder()
.withUrl("/newshub")
.build();
this.hub.on("servermessage", (m: string) => { console.log(m); });
this.hub.start()
.then(() => console.log('MessageHub Connected'))
.catch(err => console.log('MessageHub Connection Error: ' + err.toString()));
}, error => console.error(error));
}
SignalR hub 授权失败。输出 window:
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: sub preferred_username name
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[2021-08-01T15:43:11.337Z] Information: Normalizing '/newshub' to 'https://localhost:44306/newshub'.
Failed to load resource: the server responded with a status of 401 () [https://localhost:44306/newshub/negotiate?negotiateVersion=1]
[2021-08-01T15:43:11.347Z] Error: Failed to complete negotiation with the server: Error
[2021-08-01T15:43:11.347Z] Error: Failed to start the connection: Error
MessageHub Connection Error: Error
如果我删除 [Authorize] 属性 - 它工作正常
编辑 有人说我使用 cookie 但需要不记名令牌。这不是真的。当我尝试在中心 class 上将 Cookie 指定为授权方案时,出现此错误:
System.InvalidOperationException: No authentication handler is registered for the scheme 'Cookies'.
The registered schemes are: Identity.Application, Identity.External, Identity.TwoFactorRememberMe, Identity.TwoFactorUserId, idsrv, idsrv.external, IdentityServerJwt, IdentityServerJwtBearer.
如果您想使用信号客户端将令牌传递给后端集线器,您应该提供访问令牌而不是使用 cookie。服务器验证令牌并使用它来识别用户。此验证仅在建立连接时执行。在连接的生命周期内,服务器不会自动重新验证以检查令牌撤销。
具体可以阅读微软官方文档:
在尝试解决身份验证几个小时后,一位 aspnetcore 开发人员让我相信没有 simple/automatic 方法可以通过 angular 实现信号器授权,而无需 手动重新实现身份和绕过所有身份服务器便利..
所以我发明了这个解决方法。
安全性由连接 Identity Server userId 和 SignalR connectionId 的控制器授权提供。
控制器
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
{
this.hub = hub;
this.logger = logger;
}
[HttpGet]
[Route("{connectionId}")]
public IEnumerable<WeatherForecast> GetForSignalR(string connectionId)
{
SurrogateAuth(connectionId);
// NB: in real app - send particular data to particular users (by connection)
var timerManager = new TimerManager(() => hub.Clients.Client(NewsHub.Connected.Keys.First()).SendAsync("servermessage", DateTime.Now.Ticks.ToString()));
. . .
private void SurrogateAuth(string connectionId)
{
var userId = GetApiUserSimple(this.HttpContext);
NewsHub.Connected[connectionId].UserId = userId;
}
public static string GetApiUserSimple(HttpContext httpContext)
{
System.Security.Claims.ClaimsPrincipal currentUser = httpContext.User;
var userId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
return userId;
}
枢纽
public class NewsHub : Hub
{
public static readonly SortedDictionary<string, HubAuthItem> Connected = new SortedDictionary<string, HubAuthItem>();
public override Task OnConnectedAsync()
{
NewsHub.Connected.Add(Context.ConnectionId, new HubAuthItem { ConnectionId = Context.ConnectionId, LastConnect = DateTime.Now });
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Connected.Remove(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
}
控制器
constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
this.hub = new HubConnectionBuilder()
.withUrl("/newshub")
.build();
this.hub.on("servermessage", (m: string) => { console.log(m); });
this.hub.start()
.then(() => {
console.log(`MessageHub Connected: ${this.hub.connectionId}`);
http.get<WeatherForecast[]>(baseUrl + 'weatherforecast/' + this.hub.connectionId).subscribe(result => {
this.forecasts = result;
}, error => console.log('Weather get error: ' + stringify(error)));
})
.catch(err => console.log('MessageHub connection error: ' + stringify(err)));
}
有一个明显的解决方案。我认为这是@Chaodeng 和@Stilgar 所说的只是我被阅读太多博客蒙蔽了双眼。这是在创建具有身份的 asp.net 核心 angular 应用程序后可以使用的确切代码:
客户端:
import { AuthorizeService } from '../../api-authorization/authorize.service';
. . .
constructor(. . . , authsrv: AuthorizeService) {
this.hub = new HubConnectionBuilder()
.withUrl("/newshub", { accessTokenFactory: () => authsrv.getAccessToken().toPromise() })
.build();
服务器端:
[Authorize]
public class NewsHub : Hub
请注意 - 这仅适用于服务器端身份的情况(即 IdentityServer4 创建令牌,而不是 Angular)
创建了全新的 asp.net core 5 angular spa 应用表单模板:
dotnet new angular --auth Individual
npm i @microsoft/signalr
已修改Startup.cs
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder => builder
.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddSignalR();
. . .
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
. . .
endpoints.MapHub<NewsHub>("/newshub");
});
已添加中心 class
[Authorize]
public class NewsHub : Hub
{
}
修改后的 WeatherForecastController:
private IHubContext<NewsHub> _hub;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
{
_hub = hub;
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var timerManager = new TimerManager(() =>
_hub.Clients.All.SendAsync("servermessage", DateTime.Now.Ticks.ToString()));
修改fetch-data.component.ts
constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
http.get<WeatherForecast[]>(baseUrl + 'weatherforecast').subscribe(result => {
this.forecasts = result;
this.hub = new HubConnectionBuilder()
.withUrl("/newshub")
.build();
this.hub.on("servermessage", (m: string) => { console.log(m); });
this.hub.start()
.then(() => console.log('MessageHub Connected'))
.catch(err => console.log('MessageHub Connection Error: ' + err.toString()));
}, error => console.error(error));
}
SignalR hub 授权失败。输出 window:
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.ResponseHandling.UserInfoResponseGenerator: Information: Profile service returned the following claim types: sub preferred_username name
IdentityServer4.Hosting.IdentityServerMiddleware: Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[2021-08-01T15:43:11.337Z] Information: Normalizing '/newshub' to 'https://localhost:44306/newshub'.
Failed to load resource: the server responded with a status of 401 () [https://localhost:44306/newshub/negotiate?negotiateVersion=1]
[2021-08-01T15:43:11.347Z] Error: Failed to complete negotiation with the server: Error
[2021-08-01T15:43:11.347Z] Error: Failed to start the connection: Error
MessageHub Connection Error: Error
如果我删除 [Authorize] 属性 - 它工作正常
编辑 有人说我使用 cookie 但需要不记名令牌。这不是真的。当我尝试在中心 class 上将 Cookie 指定为授权方案时,出现此错误:
System.InvalidOperationException: No authentication handler is registered for the scheme 'Cookies'.
The registered schemes are: Identity.Application, Identity.External, Identity.TwoFactorRememberMe, Identity.TwoFactorUserId, idsrv, idsrv.external, IdentityServerJwt, IdentityServerJwtBearer.
如果您想使用信号客户端将令牌传递给后端集线器,您应该提供访问令牌而不是使用 cookie。服务器验证令牌并使用它来识别用户。此验证仅在建立连接时执行。在连接的生命周期内,服务器不会自动重新验证以检查令牌撤销。
具体可以阅读微软官方文档:
在尝试解决身份验证几个小时后,一位 aspnetcore 开发人员让我相信没有 simple/automatic 方法可以通过 angular 实现信号器授权,而无需 手动重新实现身份和绕过所有身份服务器便利..
所以我发明了这个解决方法。
安全性由连接 Identity Server userId 和 SignalR connectionId 的控制器授权提供。
控制器
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
public WeatherForecastController(ILogger<WeatherForecastController> logger, IHubContext<NewsHub> hub)
{
this.hub = hub;
this.logger = logger;
}
[HttpGet]
[Route("{connectionId}")]
public IEnumerable<WeatherForecast> GetForSignalR(string connectionId)
{
SurrogateAuth(connectionId);
// NB: in real app - send particular data to particular users (by connection)
var timerManager = new TimerManager(() => hub.Clients.Client(NewsHub.Connected.Keys.First()).SendAsync("servermessage", DateTime.Now.Ticks.ToString()));
. . .
private void SurrogateAuth(string connectionId)
{
var userId = GetApiUserSimple(this.HttpContext);
NewsHub.Connected[connectionId].UserId = userId;
}
public static string GetApiUserSimple(HttpContext httpContext)
{
System.Security.Claims.ClaimsPrincipal currentUser = httpContext.User;
var userId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
return userId;
}
枢纽
public class NewsHub : Hub
{
public static readonly SortedDictionary<string, HubAuthItem> Connected = new SortedDictionary<string, HubAuthItem>();
public override Task OnConnectedAsync()
{
NewsHub.Connected.Add(Context.ConnectionId, new HubAuthItem { ConnectionId = Context.ConnectionId, LastConnect = DateTime.Now });
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
Connected.Remove(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
}
控制器
constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) {
this.hub = new HubConnectionBuilder()
.withUrl("/newshub")
.build();
this.hub.on("servermessage", (m: string) => { console.log(m); });
this.hub.start()
.then(() => {
console.log(`MessageHub Connected: ${this.hub.connectionId}`);
http.get<WeatherForecast[]>(baseUrl + 'weatherforecast/' + this.hub.connectionId).subscribe(result => {
this.forecasts = result;
}, error => console.log('Weather get error: ' + stringify(error)));
})
.catch(err => console.log('MessageHub connection error: ' + stringify(err)));
}
有一个明显的解决方案。我认为这是@Chaodeng 和@Stilgar 所说的只是我被阅读太多博客蒙蔽了双眼。这是在创建具有身份的 asp.net 核心 angular 应用程序后可以使用的确切代码:
客户端:
import { AuthorizeService } from '../../api-authorization/authorize.service';
. . .
constructor(. . . , authsrv: AuthorizeService) {
this.hub = new HubConnectionBuilder()
.withUrl("/newshub", { accessTokenFactory: () => authsrv.getAccessToken().toPromise() })
.build();
服务器端:
[Authorize]
public class NewsHub : Hub