You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 03:27:06