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

ASP.NET Core 8中基于用户名动态切换Azure AD B2C自定义策略登录

解决方案:ASP.NET Core 8 + Azure AD B2C 动态策略统一登录页

核心思路

通过调整认证流程起始逻辑,让未认证用户先进入自定义统一登录页输入用户名,再根据邮箱域名判断触发对应Azure AD B2C自定义策略的认证挑战,而非直接跳转至Azure。这并非Microsoft.Identity.Web的限制,而是需要修改默认的认证跳转规则。


步骤1:配置多策略认证方案

在Program.cs中注册员工/客户两个B2C自定义策略的认证方案,同时修改Cookie认证的登录路径指向自定义页:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

// 添加MVC和Razor Pages服务
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

// 注册两个B2C自定义策略的认证方案
builder.Services.AddAuthentication()
    // 员工专属认证方案:"EmployeePolicy"
    .AddMicrosoftIdentityWebApp(options =>
    {
        builder.Configuration.Bind("AzureAdB2C:Employee", options);
    }, authenticationScheme: "EmployeePolicy", cookieScheme: "EmployeeCookie")
    // 客户专属认证方案:"CustomerPolicy"
    .AddMicrosoftIdentityWebApp(options =>
    {
        builder.Configuration.Bind("AzureAdB2C:Customer", options);
    }, authenticationScheme: "CustomerPolicy", cookieScheme: "CustomerCookie");

// 配置Cookie中间件,将未认证用户引导至自定义登录页
builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
    options.LoginPath = "/Account/SelectLogin";
});

// 配置每个策略的OpenIdConnect事件,传递login_hint参数到B2C
builder.Services.Configure<OpenIdConnectOptions>("EmployeePolicy", options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        if (context.Properties.Items.TryGetValue("login_hint", out var loginHint))
        {
            context.ProtocolMessage.LoginHint = loginHint;
        }
        return Task.CompletedTask;
    };
});

builder.Services.Configure<OpenIdConnectOptions>("CustomerPolicy", options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        if (context.Properties.Items.TryGetValue("login_hint", out var loginHint))
        {
            context.ProtocolMessage.LoginHint = loginHint;
        }
        return Task.CompletedTask;
    };
});

var app = builder.Build();

// 中间件配置
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

步骤2:创建自定义登录页控制器

添加AccountController,提供允许匿名访问的登录选择页,处理用户名输入并动态触发对应策略的认证:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

[AllowAnonymous]
public class AccountController : Controller
{
    private readonly IOptionsSnapshot<MicrosoftIdentityOptions> _optionsSnapshot;

    public AccountController(IOptionsSnapshot<MicrosoftIdentityOptions> optionsSnapshot)
    {
        _optionsSnapshot = optionsSnapshot;
    }

    [HttpGet]
    public IActionResult SelectLogin(string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        return View();
    }

    [HttpPost]
    public IActionResult SelectLogin(string username, string returnUrl = null)
    {
        if (string.IsNullOrWhiteSpace(username))
        {
            ModelState.AddModelError("", "请输入用户名");
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        // 根据邮箱域名判断使用的策略(示例:员工邮箱后缀为@company.com)
        string authScheme = username.EndsWith("@company.com", StringComparison.OrdinalIgnoreCase) 
            ? "EmployeePolicy" 
            : "CustomerPolicy";

        // 构造认证属性,传递login_hint和原返回URL
        var authProperties = new AuthenticationProperties
        {
            RedirectUri = returnUrl ?? Url.Content("~/"),
            Items = { { "login_hint", username } }
        };

        // 触发对应策略的认证挑战
        return Challenge(authProperties, authScheme);
    }
}

步骤3:创建登录页视图

在Views/Account目录下添加SelectLogin.cshtml,实现简单的用户名输入表单:

@{
    ViewData["Title"] = "统一登录";
}

<div class="container mt-5">
    <h2>统一登录</h2>
    <form method="post" class="mt-3">
        <div class="mb-3">
            <label for="username" class="form-label">用户名(邮箱)</label>
            <input type="email" class="form-control" id="username" name="username" required>
        </div>
        <input type="hidden" name="returnUrl" value="@ViewData["ReturnUrl"]">
        <button type="submit" class="btn btn-primary">下一步</button>
    </form>
</div>

关键说明

  1. 为什么之前的方法无效?

    • 默认使用OpenIdConnect作为认证方案时,未认证用户会直接触发OpenIdConnect的Challenge跳转至Azure。通过修改Cookie的LoginPath,我们将初始跳转拦截到自定义页,再手动触发对应策略的认证流程。
    • RedirectToIdentityProvider事件是在进入OpenIdConnect流程后触发的,无法拦截初始的未认证跳转。
  2. 页面保护逻辑

    • 所有业务页面仍保留[Authorize]属性,未认证用户访问时会被Cookie中间件引导至/Account/SelectLogin(该页已配置[AllowAnonymous])。
  3. 配置文件参考
    在appsettings.json中添加两个策略的配置:

    "AzureAdB2C": {
      "Employee": {
        "Instance": "https://yourtenant.b2clogin.com/",
        "Domain": "yourtenant.onmicrosoft.com",
        "TenantId": "你的租户ID",
        "ClientId": "员工端应用ClientID",
        "CallbackPath": "/signin-oidc-employee",
        "SignUpSignInPolicyId": "B2C_1A_Employee_SignIn"
      },
      "Customer": {
        "Instance": "https://yourtenant.b2clogin.com/",
        "Domain": "yourtenant.onmicrosoft.com",
        "TenantId": "你的租户ID",
        "ClientId": "客户端应用ClientID",
        "CallbackPath": "/signin-oidc-customer",
        "SignUpSignInPolicyId": "B2C_1A_Customer_SignIn"
      }
    }
    

内容的提问来源于stack exchange,提问作者Mythprod

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:03:14