You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何用ASP.NET Core和Identity实现两级身份认证(密码+用户选择)

没问题,我来帮你搭建这个基于ASP.NET Core Identity的两级认证流程,完全匹配你的需求:先通过全局密码验证,再选择用户登录,登出后直接回到用户选择界面无需重复输入全局密码,同时适配公网访问的安全要求。

核心设计思路

我们把整个流程拆成两层:

  1. 全局门禁验证:相当于家庭大门的密码,只有输入正确才能进入用户选择环节,验证通过后会生成一个长期有效的自定义Cookie(比如GlobalAuth),用来标识你已经通过了第一道关卡。
  2. 用户身份登录:在通过全局验证后,你可以从列表里选具体的用户账号,直接用Identity完成该用户的登录(跳过用户自己的密码,因为全局验证已经把好了第一道关)。
  3. 登出逻辑:登出时只清除Identity生成的用户登录Cookie,保留全局验证的Cookie,这样下次直接回到用户选择页就行,不用再输全局密码。
分步实现指南

1. 配置全局验证的基础结构

首先我们需要一个全局登录页面和对应的控制器,用来处理全局密码的验证和Cookie的生成:

  • 全局密码建议存在appsettings.json里(别硬编码!),比如加个"GlobalAuth": { "Password": "你的复杂全局密码" }节点。
  • 创建一个GlobalAuthController,负责全局密码的验证和跳转逻辑。

2. 改造用户选择与登录流程

  • 创建用户选择页面,用UserManager<IdentityUser>获取系统里的所有用户列表展示出来。
  • 用户选择账号后,直接调用SignInManager.SignInAsync完成登录,不需要验证该用户的密码(因为已经过了全局验证)。
  • 给用户选择页面添加授权策略,确保只有持有全局验证Cookie的用户才能访问。

3. 定制登出逻辑

默认的登出会清除所有认证Cookie,我们需要修改它,只清除Identity的用户登录Cookie,保留全局验证的Cookie,这样登出后直接跳回用户选择页。

4. 安全适配公网访问

  • 强制开启HTTPS:公网访问必须用HTTPS,防止密码明文传输,家庭内网可以用Let's Encrypt申请免费证书,路由器做端口转发。
  • 给全局验证Cookie设置HttpOnlySecureSameSite属性,防止XSS和CSRF攻击。
  • 可选:给全局验证添加验证码或错误次数锁定机制,防止暴力破解。
关键代码示例

全局验证控制器

public class GlobalAuthController : Controller
{
    private const string GlobalAuthCookieName = "GlobalAuth";
    private readonly IConfiguration _config;

    public GlobalAuthController(IConfiguration config)
    {
        _config = config;
    }

    [HttpGet]
    public IActionResult Login()
    {
        // 如果已经有全局Cookie,直接跳去用户选择页
        if (Request.Cookies.ContainsKey(GlobalAuthCookieName))
        {
            return RedirectToAction("SelectUser", "Account");
        }
        return View();
    }

    [HttpPost]
    public IActionResult Login(string globalPassword)
    {
        var validPassword = _config["GlobalAuth:Password"];
        if (globalPassword == validPassword)
        {
            var cookieOptions = new CookieOptions
            {
                HttpOnly = true,
                Secure = true, // 生产环境必须设为true,本地测试可以暂时设为false
                Expires = DateTimeOffset.UtcNow.AddDays(7), // 全局Cookie有效期设7天,可调整
                SameSite = SameSiteMode.Lax
            };
            Response.Cookies.Append(GlobalAuthCookieName, "Authenticated", cookieOptions);
            return RedirectToAction("SelectUser", "Account");
        }
        ModelState.AddModelError("", "全局密码错误");
        return View();
    }
}

用户选择与登录控制器

[Authorize(Policy = "GlobalAuthenticated")]
public class AccountController : Controller
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly SignInManager<IdentityUser> _signInManager;

    public AccountController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpGet]
    public async Task<IActionResult> SelectUser()
    {
        var users = await _userManager.Users.ToListAsync();
        return View(users);
    }

    [HttpPost]
    public async Task<IActionResult> SelectUser(string userId)
    {
        var user = await _userManager.FindByIdAsync(userId);
        if (user == null)
        {
            ModelState.AddModelError("", "所选用户不存在");
            var users = await _userManager.Users.ToListAsync();
            return View(users);
        }
        // 直接登录该用户,跳过密码验证
        await _signInManager.SignInAsync(user, isPersistent: false);
        return RedirectToAction("Index", "Home");
    }

    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        // 只清除Identity的用户登录Cookie,保留全局验证Cookie
        await _signInManager.SignOutAsync();
        return RedirectToAction("SelectUser");
    }
}

配置授权策略与中间件(Program.cs)

// 添加全局验证的授权策略
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("GlobalAuthenticated", policy =>
        policy.RequireAssertion(context =>
            context.HttpContext.Request.Cookies.ContainsKey("GlobalAuth")));
});

// 配置Identity服务
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = false)
    .AddEntityFrameworkStores<ApplicationDbContext>();

// 中间件顺序很重要!全局验证逻辑要放在Identity之前
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

// 全局验证拦截中间件:未持有全局Cookie的用户,强制跳转到全局登录页
app.Use(async (context, next) =>
{
    var path = context.Request.Path;
    // 排除全局登录页和静态资源路径
    if (!path.StartsWithSegments("/GlobalAuth/Login") && 
        !path.StartsWithSegments("/css") && 
        !path.StartsWithSegments("/js") &&
        !context.Request.Cookies.ContainsKey("GlobalAuth"))
    {
        context.Response.Redirect("/GlobalAuth/Login");
        return;
    }
    await next();
});

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
额外注意事项
  • 全局密码一定要设置得足够复杂,因为这是公网访问的第一道防线。
  • 家庭内网的话,可以额外添加IP白名单限制,只允许内网IP访问全局登录页,进一步提升安全性。
  • 如果需要让用户自己设置密码(而不是完全跳过),可以在用户选择后再加一步用户密码验证,但根据你的需求,目前的方案已经足够。

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

火山引擎 最新活动