名称: dotnet-blazor-auth 描述: “实现Blazor认证流程:登录/注销、AuthorizeView、Identity UI、OIDC。” 用户可调用: false
dotnet-blazor-auth
在所有Blazor托管模型中实现认证和授权。涵盖AuthorizeView、CascadingAuthenticationState、Identity UI脚手架、基于角色/策略的授权、每个托管模型的认证流程差异(cookie vs token)以及外部身份提供商。
范围
- 每个Blazor托管模型的认证流程(Server、WASM、Auto、SSR、Hybrid)
- AuthorizeView和CascadingAuthenticationState模式
- Identity UI脚手架和自定义
- Blazor中基于角色/策略的授权
- 客户端令牌处理和外部身份提供商
- Blazor应用的显式登录/注销/认证UI实现任务
超出范围
- JWT令牌生成和验证 – 参见[技能:dotnet-api-security]
- OWASP安全原则 – 参见[技能:dotnet-security-owasp]
- 无认证流程工作的CSRF/XSS/CSP/速率限制加固 – 参见[技能:dotnet-security-owasp]
- 无认证流程实现更改的现有登录页面仅加固审查 – 参见[技能:dotnet-security-owasp]
- bUnit测试认证组件 – 参见[技能:dotnet-blazor-testing]
- E2E认证测试 – 参见[技能:dotnet-playwright]
- UI框架选择 – 参见[技能:dotnet-ui-chooser]
交叉引用:[技能:dotnet-api-security]用于API级认证,[技能:dotnet-security-owasp]用于OWASP原则,[技能:dotnet-blazor-patterns]用于托管模型,[技能:dotnet-blazor-components]用于组件架构,[技能:dotnet-blazor-testing]用于bUnit测试,[技能:dotnet-playwright]用于E2E测试,[技能:dotnet-ui-chooser]用于框架选择。
路由说明:除非任务明确包括Blazor认证流程/UI实现,否则不要为OWASP加固审查加载此技能。
每个托管模型的认证流程
Blazor托管模型中的认证模式有显著差异:
| 关注点 | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Static SSR | Hybrid |
|---|---|---|---|---|---|
| 认证机制 | 基于cookie(服务器端) | 基于令牌(JWT/OIDC) | Cookie(Server阶段),Token(WASM阶段) | 基于cookie(标准ASP.NET Core) | 平台原生或cookie |
| 用户状态访问 | 直接HttpContext访问 |
AuthenticationStateProvider |
按阶段变化 | HttpContext |
平台认证API |
| 令牌存储 | 不需要(cookie) | localStorage或sessionStorage |
从cookie过渡到token | 不需要(cookie) | 安全存储(Keychain等) |
| 刷新处理 | 电路重新连接 | 通过拦截器刷新令牌 | 自动 | 标准cookie续订 | 平台特定 |
InteractiveServer认证
服务器端Blazor使用cookie认证。用户通过标准ASP.NET Core登录流程认证,cookie随初始HTTP请求发送以建立SignalR电路。
// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorization();
注意: HttpContext在初始HTTP请求期间可用,但在SignalR电路建立后,在交互式组件的生命周期方法中为null。不要在交互式组件生命周期方法中访问HttpContext。使用AuthenticationStateProvider代替。
InteractiveWebAssembly认证
WASM在浏览器中运行。Cookie认证适用于同源API(和Backend-for-Frontend / BFF模式),但基于令牌的认证(OIDC/JWT)是跨源API和委派访问场景的标准方法:
// 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");
});
// 使用BaseAddressAuthorizationMessageHandler将令牌附加到API调用
// (自动为应用基地址的请求附加令牌)
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(token认证)。处理两者:
// Server Program.cs
builder.Services.AddAuthentication()
.AddCookie()
.AddJwtBearer(); // 用于过渡后的WASM API调用
builder.Services.AddCascadingAuthenticationState();
Hybrid (MAUI)认证
// 注册平台特定认证
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, MauiAuthStateProvider>();
// 使用安全存储的自定义提供程序
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>欢迎,@context.User.Identity?.Name!</p>
<a href="/Account/Logout">注销</a>
</Authorized>
<NotAuthorized>
<a href="/Account/Login">登录</a>
</NotAuthorized>
<Authorizing>
<p>检查认证中...</p>
</Authorizing>
</AuthorizeView>
基于角色
<AuthorizeView Roles="Admin,Manager">
<Authorized>
<AdminDashboard />
</Authorized>
<NotAuthorized>
<p>您没有访问管理员仪表板的权限。</p>
</NotAuthorized>
</AuthorizeView>
基于策略
<AuthorizeView Policy="CanEditProducts">
<Authorized>
<button @onclick="EditProduct">编辑</button>
</Authorized>
</AuthorizeView>
// 在Program.cs中注册策略
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanEditProducts", policy =>
policy.RequireClaim("permission", "products.edit"));
CascadingAuthenticationState
CascadingAuthenticationState将当前AuthenticationState作为级联参数提供给所有后代组件。
设置
// Program.cs -- 注册级联认证状态
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;
// 检查认证
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提供了一个完整的认证系统,包括注册、登录、电子邮件确认、密码重置和双因素认证。
将Identity添加到Blazor Web应用
# 添加Identity脚手架
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页面
# 为自定义脚手架个别Identity页面
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="邮箱" />
</div>
<div>
<InputText @bind-Value="loginModel.Password" type="password" placeholder="密码" />
</div>
<div>
<InputCheckbox @bind-Value="loginModel.RememberMe" /> 记住我
</div>
<button type="submit">登录</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 = "账户已锁定。请稍后重试。";
}
else
{
errorMessage = "无效登录尝试。";
}
}
}
注意: SignInManager使用HttpContext设置cookie。在交互式渲染模式中,电路建立后HttpContext不可用。登录/注销页面必须使用Static SSR(无@rendermode)以便访问HttpContext进行cookie操作。
基于角色和策略的授权
页面级授权
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h1>管理员面板</h1>
@page "/products/manage"
@attribute [Authorize(Policy = "ProductManager")]
<h1>管理产品</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;
}
}
// 注册
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;
代理注意事项
- 不要在交互式组件中访问
HttpContext。HttpContext仅在初始HTTP请求期间可用。SignalR电路建立后(InteractiveServer)或WASM运行时加载后,其为null。使用AuthenticationStateProvider或CascadingAuthenticationState代替。 - 在WASM中不要依赖cookie进行跨源或委派API访问。 对于跨源API,使用带
AuthorizationMessageHandler的OIDC/JWT。对于WASM应用,同源和Backend-for-Frontend(BFF)cookie认证仍然有效。 - 不要在交互式模式中渲染登录/注销页面。
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应用,
AddCascadingAuthenticationState服务注册) Microsoft.AspNetCore.Identity.EntityFrameworkCore用于Identity与EF CoreMicrosoft.AspNetCore.Identity.UI用于默认Identity UI脚手架Microsoft.AspNetCore.Authentication.MicrosoftAccount/.Google用于外部提供商Microsoft.Authentication.WebAssembly.Msal用于WASM与Microsoft Identity(Azure AD/Entra)