SignalR 授权无法在带有 Identity Server 的 asp.net core angular SPA 中开箱即用

Posted

技术标签:

【中文标题】SignalR 授权无法在带有 Identity Server 的 asp.net core angular SPA 中开箱即用【英文标题】:SignalR authorization not working out of the box in asp.net core angular SPA with Identity Server 【发布时间】:2021-10-07 08:56:31 【问题描述】:

请注意 - 这仅适用于服务器端身份的情况(即 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");
            );

添加 Hub 类

    [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 集线器的授权失败。输出窗口:

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] 属性 - 它工作正常

EDIT 对那些说我使用 cookie 但需要不记名令牌的人。这不是真的。当我尝试将 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.

【问题讨论】:

【参考方案1】:

如果您想使用信号客户端将令牌传递到后端集线器,您应该提供访问令牌而不是使用 cookie。服务器验证令牌并使用它来识别用户。此验证仅在建立连接时执行。在连接的生命周期内,服务器不会自动重新验证以检查令牌撤销。

详情可以阅读微软官方文档:

https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#identity-server-jwt-authentication

【讨论】:

是的,谢谢.. 但是.. 我之前已经有两次完全相同的答案.. 请在此处阅读我的回复:docs.microsoft.com/en-us/answers/questions/497227/… - 我不知道如何访问令牌,并且同一文件还承诺 SignalR 必须使用 cookie 自动授权(再次 - 请参阅该链接) P.S. If you want to use the signal client to pass the token - 我只是希望信号器获得授权。怎么 - 你告诉我:) 为什么不添加cookie auth? 因为我不知道怎么做 好吧,你需要尝试一下。同意反对意见。【参考方案2】:

在尝试解决身份验证问题几个小时后,一位 aspnetcore 开发人员让我相信,如果不手动重新实现身份并绕过所有身份服务器便利强>..

所以我发明了这个解决方法

安全性由连接 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)));
  

【讨论】:

【参考方案3】:

有一个明显的解决方案。我认为这是@Chaodeng 和@Stilgar 所说的,只是我被阅读太多博客蒙蔽了双眼。这是创建具有身份的 asp.net core 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

【讨论】:

这绝对不是问这个问题的最佳地方,但如果您知道,对于 Blazor Webassembly 应用程序来说,这会是什么样子?我尝试了多种方法,这个问题是我发现的最接近的问题。 @PeterLenjo 有 github 存储库,您可以在其中尝试询问。无论哪种方式,你都应该在这里问它——只要确保你详细解释它是如何不起作用的。我个人不知道,因为我从未使用过 blazor.. 谢谢你,我遵循了这一点,最终找到了帮助我的 WPF 示例。

以上是关于SignalR 授权无法在带有 Identity Server 的 asp.net core angular SPA 中开箱即用的主要内容,如果未能解决你的问题,请参考以下文章

SignalR Core 中默认授权所有集线器

为啥 SignalR Context.User.Identity.Name 为空?

ASP.NET Core Angular - 根据登录用户发送不同的 SignalR 消息

无法让 Identity Server 4 API 授权其自己的端点,而是发送登录页面

如何向已经使用 Identity 的 MVC 控制器添加 API 身份验证和授权?

如何在 ng2-signalr 中设置授权标头?