基于ASP.NET Core后端的Blazor/MAUI Hybrid身份认证与授权最优方案咨询
统一身份认证与授权解决方案(适配Blazor WASM + MAUI Blazor Hybrid)
核心思路
后端同时启用Cookie认证(适配Blazor WASM网站)和JWT Bearer认证(适配MAUI Blazor Hybrid应用),完全复用Microsoft Identity的内置特性(包括外部登录、持久化登录),无需依赖第三方身份提供商。
后端配置步骤
1. 同时启用Cookie与JWT Bearer认证
在Program.cs中配置Identity服务时,同时注册两种认证方案:
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // 配置认证服务,同时支持Cookie和JWT builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) // Cookie认证(给Blazor WASM) .AddCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.ExpireTimeSpan = TimeSpan.FromDays(30); // 持久化登录时长 options.SlidingExpiration = true; // 用户活跃时自动续期 }) // JWT Bearer认证(给MAUI Hybrid) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; // 支持MAUI端通过URL参数传递Token(可选) options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/api")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); // 全局授权策略:默认要求认证用户 builder.Services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); });
2. 新增JWT Token生成端点
创建API控制器,用于MAUI应用登录后获取Token:
[ApiController] [Route("api/auth")] public class AuthController : ControllerBase { private readonly UserManager<IdentityUser> _userManager; private readonly IConfiguration _configuration; public AuthController(UserManager<IdentityUser> userManager, IConfiguration configuration) { _userManager = userManager; _configuration = configuration; } [HttpPost("token")] public async Task<IActionResult> GenerateToken([FromBody] LoginRequest request) { var user = await _userManager.FindByEmailAsync(request.Email); if (user == null || !await _userManager.CheckPasswordAsync(user, request.Password)) { return Unauthorized("无效的邮箱或密码"); } // 构建用户声明 var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Email, user.Email) }; var roles = await _userManager.GetRolesAsync(user); claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); // 生成JWT Token var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddDays(30), signingCredentials: creds); return Ok(new { Token = new JwtSecurityTokenHandler().WriteToken(token) }); } public class LoginRequest { public string Email { get; set; } public string Password { get; set; } } }
Blazor WASM端处理
1. 配置Cookie认证与HttpClient
在Program.cs中注册认证服务,确保请求携带身份Cookie:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // 注册自定义认证状态提供器 builder.Services.AddScoped<AuthenticationStateProvider, CookieAuthStateProvider>(); builder.Services.AddAuthorizationCore(); // 配置携带Cookie的HttpClient builder.Services.AddScoped<CookieHandler>(); builder.Services.AddHttpClient("ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler<CookieHandler>(); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ServerAPI"));
2. 自定义AuthenticationStateProvider
复用Cookie状态判断用户认证状态:
public class CookieAuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; public CookieAuthStateProvider(HttpClient httpClient) { _httpClient = httpClient; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { try { // 调用后端端点验证当前Cookie是否有效 var response = await _httpClient.GetAsync("/api/auth/check"); if (response.IsSuccessStatusCode) { var userInfo = await response.Content.ReadFromJsonAsync<UserInfo>(); var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, userInfo.Id), new Claim(ClaimTypes.Email, userInfo.Email) }; var identity = new ClaimsIdentity(claims, "cookie"); return new AuthenticationState(new ClaimsPrincipal(identity)); } } catch { } return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } public class UserInfo { public string Id { get; set; } public string Email { get; set; } } }
3. CookieHandler确保请求携带Cookie
public class CookieHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 从浏览器Cookie中获取身份认证Cookie using var handler = new HttpClientHandler(); using var client = new HttpClient(handler) { BaseAddress = request.RequestUri.GetLeftPart(UriPartial.Authority) }; await client.GetAsync("/"); var cookieHeader = handler.CookieContainer.GetCookieHeader(request.RequestUri); if (!string.IsNullOrEmpty(cookieHeader)) { request.Headers.Add("Cookie", cookieHeader); } return await base.SendAsync(request, cancellationToken); } }
MAUI Blazor Hybrid端处理
1. 安全存储JWT Token
使用MAUI内置的SecureStorage存储Token,避免明文泄露:
public static class TokenManager { public static async Task SaveTokenAsync(string token) { await SecureStorage.SetAsync("auth_token", token); } public static async Task<string> GetTokenAsync() { return await SecureStorage.GetAsync("auth_token"); } public static async Task ClearTokenAsync() { await SecureStorage.RemoveAsync("auth_token"); } }
2. 自定义AuthenticationStateProvider
基于JWT Token判断认证状态:
public class MauiAuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; public MauiAuthStateProvider(HttpClient httpClient) { _httpClient = httpClient; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await TokenManager.GetTokenAsync(); if (string.IsNullOrEmpty(token)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // 验证Token有效性(可选,调用后端端点) var request = new HttpRequestMessage(HttpMethod.Get, "/api/auth/check"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { // 解析Token声明 var handler = new JwtSecurityTokenHandler(); var jwtToken = handler.ReadJwtToken(token); var claims = jwtToken.Claims.ToList(); var identity = new ClaimsIdentity(claims, "bearer"); return new AuthenticationState(new ClaimsPrincipal(identity)); } else { await TokenManager.ClearTokenAsync(); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } } // 通知认证状态变更 public void NotifyStateChanged() { NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }
3. 配置HttpClient自动携带Token
在MauiProgram.cs中注册服务:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://your-backend-url") }); builder.Services.AddScoped<MauiAuthStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<MauiAuthStateProvider>()); // 添加自动注入Token的消息处理器 builder.Services.AddScoped<TokenHandler>(); builder.Services.AddHttpClient("ServerAPI", client => client.BaseAddress = new Uri("https://your-backend-url")) .AddHttpMessageHandler<TokenHandler>();
4. TokenHandler实现
public class TokenHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var token = await TokenManager.GetTokenAsync(); if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } return await base.SendAsync(request, cancellationToken); } }
外部登录集成(Google/Apple)
1. 后端配置外部登录提供商
在Program.cs中添加:
builder.Services.AddAuthentication() .AddGoogle(options => { options.ClientId = builder.Configuration["Auth:Google:ClientId"]; options.ClientSecret = builder.Configuration["Auth:Google:ClientSecret"]; options.CallbackPath = "/signin-google"; }) .AddApple(options => { options.ClientId = builder.Configuration["Auth:Apple:ClientId"]; // Apple ClientSecret需要通过开发者密钥生成,可封装工具类实现 options.ClientSecret = new AppleSecretGenerator( builder.Configuration["Auth:Apple:TeamId"], builder.Configuration["Auth:Apple:KeyId"], builder.Configuration["Auth:Apple:PrivateKey"]).Generate(); options.CallbackPath = "/signin-apple"; });
2. 处理外部登录回调
在Account控制器的回调方法中,根据请求来源返回Cookie或Token:
[HttpGet] [AllowAnonymous] public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) return BadRequest($"第三方登录错误:{remoteError}"); var loginInfo = await _signInManager.GetExternalLoginInfoAsync(); if (loginInfo == null) return RedirectToAction("Login"); // 执行外部登录 var result = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, isPersistent: true); if (result.Succeeded) { var user = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); // 判断是否为MAUI请求(通过自定义Header或查询参数) var isMaui = Request.Headers.ContainsKey("X-Maui-App") || Request.Query.ContainsKey("maui"); if (isMaui) { // 生成JWT Token返回给MAUI var token = GenerateJwtToken(user); // 复用之前的Token生成逻辑 return Ok(new { Token = token }); } else { // 设置Cookie给WASM return LocalRedirect(returnUrl ?? "/"); } } // 处理未注册用户的自动注册 var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email); var newUser = new IdentityUser { UserName = email, Email = email }; var createResult = await _userManager.CreateAsync(newUser); if (createResult.Succeeded) { await _userManager.AddLoginAsync(newUser, loginInfo); await _signInManager.SignInAsync(newUser, isPersistent: true); var isMaui = Request.Headers.ContainsKey("X-Maui-App") || Request.Query.ContainsKey("maui"); if (isMaui) { var token = GenerateJwtToken(newUser); return Ok(new { Token = token }); } else { return LocalRedirect(returnUrl ?? "/"); } } return BadRequest("第三方登录后创建用户失败"); }
3. MAUI端发起外部登录
使用WebAuthenticator打开第三方登录页面,获取Token:
public async Task LoginWithGoogleAsync() { var authUrl = new Uri("https://your-backend-url/signin-google?maui=true"); var callbackUrl = new Uri("your-maui-scheme://callback"); // 需在MAUI项目配置中注册此Scheme var result = await WebAuthenticator.AuthenticateAsync(authUrl, callbackUrl); var token = result.Properties["Token"]; await TokenManager.SaveTokenAsync(token); var authStateProvider = MauiProgram.Services.GetRequiredService<MauiAuthStateProvider>(); authStateProvider.NotifyStateChanged(); }
长期登录实现
- Blazor WASM:通过Cookie的
ExpireTimeSpan设置30天有效期,开启SlidingExpiration,用户活跃时自动续期Cookie。 - MAUI Hybrid:生成有效期30天的JWT Token,存储在SecureStorage中;可选实现Token刷新端点,当Token即将过期时,用旧Token换取新Token,避免用户频繁登录。
内容的提问来源于stack exchange,提问作者user3015088
相关产品推荐
相关产品推荐

