为什么从Blazor WebAssembly到 SignalR的认证Cookie无法传递?
我正在为一个基于浏览器的游戏平台构建一个配对服务,使用Blazor WebAssembly、Microsoft Entra外部身份和SignalR。为了将客户端连接到配对服务,我创建了 Hub 类的一个实例,然后添加了 [Authorize] 属性来进行保护,但Blazor组件无法启动与hub的连接。当 HubConnection.StartAsync() 被调用时,会抛出以下异常:
System.Text.Json.JsonReaderException: '\<' is an invalid start of a value. LineNumber: 2 | BytePositionInLine: 0.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
at System.Text.Json.Utf8JsonReader.Read()
at Microsoft.AspNetCore.Internal.SystemTextJsonExtensions.CheckRead(Utf8JsonReader& reader)
at Microsoft.AspNetCore.Http.Connections.NegotiateProtocol.ParseResponse(ReadOnlySpan`1 content)
当移除 [Authorize] 属性后,同一方法不会抛出异常,与hub的连接也已建立。
问题
- 这个错误的根本原因是什么?
- 如何解决?
项目结构
该方案包含两个项目:
- Blazor服务端(Web应用)
- Blazor客户端(WebAssembly)
Blazor服务端被配置为连接到身份服务器(Microsoft Entra External ID)以登录用户,并使用浏览器Cookie来存储访问令牌和身份令牌。它还包含配对集线器和配对服务。
Blazor客户端包含试图连接到SignalR集线器的页面组件。
Blazor服务端项目包含以下 Program.cs:
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDistributedTokenCaches();
builder.Services.AddAuthorizationBuilder()
.AddPolicy("RequireAuthenticatedUser", policy => policy.RequireAuthenticatedUser());
builder.Services.AddSignalR();
builder.Services.AddResponseCompression(options => {
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat([ "application/octet-stream" ]);
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseResponseCompression();
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Client._Imports).Assembly);
app.MapGroup("/authentication").MapAuthentication();
app.MapHub<MatchHub>(MatchHub.Url);
app.Run();
}
}
SignalR集线器的实现如下:
[Authorize]
public class MatchHub : Hub<IMatchHub>
{
public const string Url = "/match";
private readonly IMatchService matchService;
public MatchHub(IMatchService matchService)
{
this.matchService = matchService;
}
public async Task FindMatch()
{
if (Context.User is null)
{
return;
}
await matchService.FindMatch(new PlayerConnection(Context.ConnectionId, Context.User));
}
}
Blazor客户端项目包含以下 Program.cs:
class Program
{
static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
await builder.Build().RunAsync();
}
}
组件页面的实现如下:
@page "/lobby"
@using Company.Client.Services.Authentication
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.SignalR.Client
@layout LobbyLayout
@attribute [Authorize]
@inject NavigationManager NavigationManager
<PageTitle>Lobby</PageTitle>
<div class="lobby">
<button class="game">
<div class="image">
<img src="@src" alt="@alt" />
</div>
<div class="caption">
<h1>game name</h1>
</div>
</button>
<button class="play">play</button>
</div>
@code {
private string src => $"/images/games/game-1/game-billboard.png";
private string alt => "game name";
private bool finding = false;
private HubConnection? hubConnection;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/match"))
.WithAutomaticReconnect()
.Build();
await hubConnection.StartAsync();
}
}
尝试解决的方法
这就是我迄今为止尝试的:
- 在WebAssembly组件中禁用预渲染
- 将渲染模式改为InteractiveServer
- 参考以下建议:客户端SignalR的跨域身份验证协商
其他类似问题
- 异常:'<'' 是无效的值起始字符:尽管此问题报告了类似的异常,但它并非由我的代码所使用的相同API引发,也不在Blazor WebAssembly组件共享cookie凭据给受保护的SignalR hub的上下文中。
解决方案
我不明白为何,但不对 hubConnection.StartAsync(); 使用await就解决了这个问题。现在连接已按预期建立。
⚠️ 失败:
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/match"))
.WithAutomaticReconnect()
.Build();
await hubConnection.StartAsync();
}
✅ 可行:
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/match"))
.WithAutomaticReconnect()
.Build();
hubConnection.StartAsync();
}
✅ 可行:
protected override void OnInitialized()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/match"))
.WithAutomaticReconnect()
.Build();
hubConnection.StartAsync();
}
✅ 可行:
private bool started = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !started)
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/match"))
.WithAutomaticReconnect()
.Build();
await hubConnection.StartAsync();
started = true;
}
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。