关于 dotnet-blazor-auth
面向 Blazor 应用开发者的认证与授权指南,覆盖不同托管模式下的认证流程、界面组件、策略授权和 Identity 页面脚手架。
主要功能
- 01按托管模式配置认证流程
- 02使用 AuthorizeView 和认证状态
- 03Identity UI 页面脚手架
- 04基于角色和策略授权
- 05外部身份提供商
- 06令牌处理指南
适用场景
- 01为 Blazor 应用添加登录和退出界面
- 02按角色或策略限制页面访问
- 03为 Server、WebAssembly 或 Hybrid 配置认证
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-blazor-auth安装后在 Claude Code 中输入/dotnet-blazor-auth确认已加载。
详细介绍
dotnet-blazor-auth
适用于所有 Blazor 托管模型的身份验证和授权。涵盖 AuthorizeView、CascadingAuthenticationState、Identity UI 基架、基于角色/策略的授权、各托管模型的身份验证流程差异(Cookie 与令牌),以及外部身份提供程序。
**范围边界:**此技能负责 Blazor 特有的身份验证 UI 模式——AuthorizeView、CascadingAuthenticationState、Identity UI 基架、客户端令牌处理,以及各托管模型的身份验证流程配置。API 级身份验证(JWT、OAuth/OIDC、通行密钥、CORS、速率限制)——请参阅 [skill:dotnet-api-security]。
**不在范围内:**JWT 令牌生成和验证——请参阅 [skill:dotnet-api-security]。OWASP 安全原则——请参阅 [skill:dotnet-security-owasp]。使用 bUnit 测试身份验证组件——请参阅 [skill:dotnet-blazor-testing]。E2E 身份验证测试——请参阅 [skill:dotnet-playwright]。UI 框架选择——请参阅 [skill:dotnet-ui-chooser]。
交叉引用:[skill:dotnet-api-security](API 级身份验证)、[skill:dotnet-security-owasp](OWASP 原则)、[skill:dotnet-blazor-patterns](托管模型)、[skill:dotnet-blazor-components](组件架构)、[skill:dotnet-blazor-testing](bUnit 测试)、[skill:dotnet-playwright](E2E 测试)、[skill:dotnet-ui-chooser](框架选择)。
各托管模型的身份验证流程
不同 Blazor 托管模型的身份验证模式差异显著:
| 关注点 | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Static SSR | Hybrid |
|---|---|---|---|---|---|
| 身份验证机制 | 基于 Cookie(服务端) | 基于令牌(JWT/OIDC) | Cookie(Server 阶段),令牌(WASM 阶段) | 基于 Cookie(标准 ASP.NET Core) | 平台原生或 Cookie |
| 用户状态访问 | 直接访问 HttpContext | AuthenticationStateProvider | 因阶段而异 | HttpContext | 平台身份验证 API |
| 令牌存储 | 无需(Cookie) | localStorage 或 sessionStorage | 从 Cookie 切换到令牌 | 无需(Cookie) | 安全存储(Keychain 等) |
| 刷新处理 | Circuit 重新连接 | 通过拦截器刷新令牌 | 自动 | 标准 Cookie 续期 | 依平台而定 |
InteractiveServer 身份验证
服务端 Blazor 使用 Cookie 身份验证。用户通过标准 ASP.NET Core 登录流程完成身份验证,建立 SignalR circuit 的初始 HTTP 请求会携带 Cookie。
// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();
**注意事项:**初始 HTTP 请求期间可以使用 HttpContext,但 SignalR circuit 建立后,交互式组件中的 HttpContext 为 null。不要在交互式组件的生命周期方法中访问 HttpContext,请改用 AuthenticationStateProvider。
InteractiveWebAssembly 身份验证
WASM 在浏览器中运行。对于同源 API(以及 Backend-for-Frontend / BFF 模式),Cookie 身份验证可正常工作;但对于跨源 API 和委托访问场景,基于令牌的身份验证(OIDC/JWT)是标准做法:
// Client Program.cs (WASM)
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = "https://login.example.com";
options.ProviderOptions.ClientId = "blazor-wasm-client";
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.DefaultScopes.Add("api");
});
// Attach tokens to API calls using BaseAddressAuthorizationMessageHandler
// (auto-attaches tokens for requests to the app's base address)
builder.Services.AddHttpClient("API", client =>
client.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler(sp =>
sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: ["https://api.example.com"],
scopes: ["api"]));
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient("API"));
InteractiveAuto 身份验证
Auto 模式以 InteractiveServer(Cookie 身份验证)启动,随后切换到 WASM(令牌身份验证)。两种情况都要处理:
// Server Program.cs
builder.Services.AddAuthentication()
.AddCookie()
.AddJwtBearer(); // For WASM API calls after transition
builder.Services.AddCascadingAuthenticationState();
Hybrid(MAUI)身份验证
// Register platform-specific auth
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, MauiAuthStateProvider>();
// Custom provider using secure storage
public class MauiAuthStateProvider : AuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await SecureStorage.Default.GetAsync("auth_token");
if (string.IsNullOrEmpty(token))
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
var claims = ParseClaimsFromJwt(token);
var identity = new ClaimsIdentity(claims, "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
}
AuthorizeView
AuthorizeView 根据用户的身份验证和授权状态有条件地呈现内容。
基本用法
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity?.Name!</p>
<a href="/Account/Logout">Log out</a>
</Authorized>
<NotAuthorized>
<a href="/Account/Login">Log in</a>
</NotAuthorized>
<Authorizing>
<p>Checking authentication...</p>
</Authorizing>
</AuthorizeView>
基于角色
<AuthorizeView Roles="Admin,Manager">
<Authorized>
<AdminDashboard />
</Authorized>
<NotAuthorized>
<p>You do not have access to the admin dashboard.</p>
</NotAuthorized>
</AuthorizeView>
基于策略
<AuthorizeView Policy="CanEditProducts">
<Authorized>
<button @onclick="EditProduct">Edit</button>
</Authorized>
</AuthorizeView>
// Register policy in Program.cs
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanEditProducts", policy =>
policy.RequireClaim("permission", "products.edit"));
CascadingAuthenticationState
CascadingAuthenticationState 将当前 AuthenticationState 作为级联参数提供给所有后代组件。
设置
// Program.cs -- register cascading auth state
builder.Services.AddCascadingAuthenticationState();
这取代了将整个应用包装在 <CascadingAuthenticationState> 中的做法(旧模式)。推荐使用基于服务的注册方式(.NET 8+)。
在组件中使用身份验证状态
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }
private string? userName;
protected override async Task OnInitializedAsync()
{
if (AuthState is not null)
{
var state = await AuthState;
userName = state.User.Identity?.Name;
}
}
}
访问声明
var state = await AuthState;
var user = state.User;
// Check authentication
if (user.Identity?.IsAuthenticated == true)
{
var email = user.FindFirst(ClaimTypes.Email)?.Value;
var roles = user.FindAll(ClaimTypes.Role).Select(c => c.Value);
var isAdmin = user.IsInRole("Admin");
}
Identity UI 基架
ASP.NET Core Identity 提供完整的身份验证系统,支持注册、登录、电子邮件确认、密码重置和双重身份验证。
向 Blazor Web App 添加 Identity
# Add Identity scaffolding
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
// Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
搭建 Identity 页面
# Scaffold individual Identity pages for customization
dotnet aspnet-codegenerator identity -dc ApplicationDbContext --files "Account.Login;Account.Register;Account.Logout"
使用 Blazor 组件自定义 Identity UI
如需完全原生的 Blazor 身份验证体验,请创建调用 Identity API 的 Blazor 组件:
@page "/Account/Login"
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager Navigation
<EditForm Model="loginModel" OnValidSubmit="HandleLogin" FormName="login" Enhance>
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<InputText @bind-Value="loginModel.Email" placeholder="Email" />
</div>
<div>
<InputText @bind-Value="loginModel.Password" type="password" placeholder="Password" />
</div>
<div>
<InputCheckbox @bind-Value="loginModel.RememberMe" /> Remember me
</div>
<button type="submit">Log in</button>
</EditForm>
@if (!string.IsNullOrEmpty(errorMessage))
{
<p class="text-danger">@errorMessage</p>
}
@code {
[SupplyParameterFromForm]
private LoginModel loginModel { get; set; } = new();
private string? errorMessage;
private async Task HandleLogin()
{
var result = await SignInManager.PasswordSignInAsync(
loginModel.Email, loginModel.Password,
loginModel.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
Navigation.NavigateTo("/", forceLoad: true);
}
else if (result.RequiresTwoFactor)
{
Navigation.NavigateTo("/Account/LoginWith2fa");
}
else if (result.IsLockedOut)
{
errorMessage = "Account is locked. Try again later.";
}
else
{
errorMessage = "Invalid login attempt.";
}
}
}
注意事项: SignInManager 使用 HttpContext 设置 Cookie。在交互式呈现模式下,建立 circuit 后将无法使用 HttpContext。登录/注销页面必须使用 Static SSR(不设置 @rendermode),以便访问 HttpContext 执行 Cookie 操作。
基于角色和策略的授权
页面级授权
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h1>Admin Panel</h1>
@page "/products/manage"
@attribute [Authorize(Policy = "ProductManager")]
<h1>Manage Products</h1>
定义策略
builder.Services.AddAuthorizationBuilder()
.AddPolicy("ProductManager", policy =>
policy.RequireRole("Admin", "ProductManager"))
.AddPolicy("CanDeleteOrders", policy =>
policy.RequireClaim("permission", "orders.delete")
.RequireAuthenticatedUser())
.AddPolicy("MinimumAge", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
自定义授权处理程序
public sealed class MinimumAgeRequirement(int minimumAge) : IAuthorizationRequirement
{
public int MinimumAge { get; } = minimumAge;
}
public sealed class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst("date_of_birth");
if (dateOfBirthClaim is not null
&& DateOnly.TryParse(dateOfBirthClaim.Value, out var dob))
{
var age = DateOnly.FromDateTime(DateTime.UtcNow).Year - dob.Year;
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
// Register
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
在组件中执行程序化授权
@inject IAuthorizationService AuthorizationService
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }
private bool canEdit;
protected override async Task OnInitializedAsync()
{
if (AuthState is not null)
{
var state = await AuthState;
var result = await AuthorizationService.AuthorizeAsync(
state.User, "CanEditProducts");
canEdit = result.Succeeded;
}
}
}
外部身份提供程序
添加外部提供程序
builder.Services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = builder.Configuration["Auth:Microsoft:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Microsoft:ClientSecret"]!;
})
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["Auth:Google:ClientId"]!;
options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]!;
});
各托管模型的外部登录流程
| 托管模型 | 流程 | 说明 |
|---|---|---|
| InteractiveServer / Static SSR | 标准 OAuth 重定向(服务端) | 回调后存储 Cookie |
| InteractiveWebAssembly | 使用 PKCE 的 OIDC(客户端) | 在浏览器中存储令牌 |
| Hybrid(MAUI) | WebAuthenticator 或 MSAL | 平台特定的安全存储 |
对于 WASM,请在客户端项目中配置 OIDC 提供程序:
// Client Program.cs
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = "https://login.microsoftonline.com/{tenant}";
options.ProviderOptions.ClientId = "{client-id}";
options.ProviderOptions.ResponseType = "code";
});
对于 MAUI Hybrid:
var result = await WebAuthenticator.Default.AuthenticateAsync(
new Uri("https://login.example.com/authorize"),
new Uri("myapp://callback"));
var token = result.AccessToken;
Agent 注意事项
- 不要在交互式组件中访问
HttpContext。HttpContext仅在初始 HTTP 请求期间可用。SignalR circuit 建立后(InteractiveServer),或 WASM 运行时加载后,它将为null。请改用AuthenticationStateProvider或CascadingAuthenticationState。 - 不要在 WASM 中依赖 Cookie 进行跨源或委托式 API 访问。 对于跨源 API,请使用 OIDC/JWT 和
AuthorizationMessageHandler。对于 WASM 应用,同源 Cookie 身份验证和 Backend-for-Frontend(BFF)Cookie 身份验证仍然有效。 - 不要以 Interactive 模式呈现登录/注销页面。
SignInManager需要HttpContext来设置/清除 Cookie。登录和注销页面必须使用 Static SSR 呈现模式。 - 如果未考虑 XSS 风险,不要将令牌存储在
localStorage中。 如果应用存在 XSS 漏洞,localStorage中的令牌可能会被盗取。请使用sessionStorage(关闭标签页时清除),或使用内置存储机制并启用 PKCE 的 OIDC 库。 - 不要忘记
AddCascadingAuthenticationState()。 如果未添加此服务,组件中的[CascadingParameter] Task<AuthenticationState>始终为null,导致身份验证检查在无提示的情况下失效。 - 不要同时使用
AddIdentity和AddDefaultIdentity。AddDefaultIdentity包含 UI 搭建,而AddIdentity不包含。请根据是否需要默认的 Identity UI 页面选择其中之一。
前置条件
- .NET 8.0+(支持呈现模式的 Blazor Web App,以及
AddCascadingAuthenticationState服务注册) - 用于通过 EF Core 实现 Identity 的
Microsoft.AspNetCore.Identity.EntityFrameworkCore - 用于搭建默认 Identity UI 的
Microsoft.AspNetCore.Identity.UI - 用于外部提供程序的
Microsoft.AspNetCore.Authentication.MicrosoftAccount/.Google - 用于通过 Microsoft Identity(Azure AD/Entra)实现 WASM 身份验证的
Microsoft.Authentication.WebAssembly.Msal