ASP.NET Core 使用 Claim 认证详解

发布时间 2023-07-27 17:00:44作者: 大西瓜3721

微软在早期 .NET Framework 时代,针对 ASP.NET 的用户登录身份认证,提供了 Forms 认证实现方案。后来在推出 ASP.NET Core 之后,采用 Claim 认证替代了 Forms 认证,两者本质上都是基于 Cookie 加解密的认证方式,学习和使用起来非常简单,比较适合在小型项目中使用,主要是方便。

假设我们现在已经创建好了一个基于 .NET5 模型-视图-控制器 的 ASP.NET Core Web 应用 (这里就不介绍创建过程了),下面我就基于 .NET5 网站,介绍一下 Claim 认证的具体实现方式:


1 修改 appsettings.json 配置文件,增加有关 Cookie 的相关配置

在实际开发场景中,我们一般将 Cookie 相关信息进行配置,所以我们增加以下配置信息:

{
  // 这里采用了两级嵌套的 json 配置方式
  "web": {
    // 用户未登录时,自动跳转的目标路由地址
    "loginUrl": "/Home/Login", 
    // Cookie 的名字
    "cookieName": "UserAuth",
    // Cookie 保存的路径,这里保存在根路径
    "cookiePath": "/",
    // Cookie 的所属的域名
    "cookieDomain": ".demo.com"
  }
}

其实上面的 json 配置中, Cookie 的保存路径,所属域名,都可以配置为空,这样不影响 Claim 认证的实现。

但是有一种情况例外,那就是万恶的 IE 浏览器。对于 IE 浏览器来说,上面的 json 配置中,必须配置好 Cookie 的保存路径和域名,否则无法生成 Cookie,无法实现 Claim 认证。

如果你实施的项目不考虑 IE 浏览器的话,那就很幸运了。但是目前国内的很多银行、政府、国企,未来很长一段时间仍然还在使用 IE 浏览器,所以有时候还得被迫考虑 IE 浏览器。


2 修改 Startup.cs 文件,增加 Claim 认证支持

需要修改两个地方 ConfigureServices 方法 和 Configure方法

在 ConfigureServices 方法 中添加 Cookie 认证方式,在 Configure方法 中使用认证和授权。

注意:Configure 方法每行代码的顺序位置很重要,新增加的两行代码(app.UseAuthentication 和 app.UseAuthorization 必须在 app.UseRouting 之后,app.UseEndpoints 之前)

public void ConfigureServices(IServiceCollection services, IConfiguration Config)
{
    services.AddControllersWithViews();

    //添加 Cookie 认证方式
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => 
    {
        //从配置节中读取配置的 Cookie 信息
        options.LoginPath = Config["web:loginUrl"];
        options.Cookie.Name = Config["web:cookieName"];
        options.Cookie.Path = Config["web:cookiePath"];
        options.Cookie.Domain = Config["web:cookieDomain"];
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.UseRouting();

    //使用认证和授权
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>{
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Web}/{action=Index}");
    });
}

3 在相关 Controller 中的 Action 方法添加 Claim 认证的代码

假设我们在登录页面,输入了用户名密码,提交到后台的 Home/Login 中,该 Action 已经在数据库中验证了用户名和密码的正确性,接下来就要生成 Claim 认证的 Cookie,代码如下:

public IActionResult Login(string name, string pwd)
{
    //......此处省略了去数据库验证用户名和密码,以及获取用户的权限的相关代码

    //可以添加额外的信息存储在cookie中
    //这里把从数据库中获取的【用户角色】和【权限码】进行存储
    var claims = new[] { new Claim("Power", power), new Claim("Role", role) };

    //这里存储【用户名】
    //这样后面就可以通过 User.Identity.Name 获取到用户名了
    GenericIdentity gi = new GenericIdentity(name);
    ClaimsIdentity ci = new ClaimsIdentity(gi, claims, 
        CookieAuthenticationDefaults.AuthenticationScheme, "", "");
    ClaimsPrincipal cp = new ClaimsPrincipal(ci);

    Task.Run(async () =>
    {
        //生成 Cookie 并保存到硬盘中,指定 Cookie 的过期时间
        await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,cp, new AuthenticationProperties()
        {
            IsPersistent = true,
            ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(60),
            AllowRefresh = true
        });
    }).Wait();
    return Content("login success");
}

前端通过 Ajax 调用后端登录方法后,如果成功,然后跳转到网站主页即可。


4 控制 Controller 和 Action 的访问

我们可以通过在 Controller 上或 Action 上增加 [Authorize] 标记 或 [AllowAnonymous] 标记,来控制用户对具体 Controller 或 Action 的访问,这跟以前的 Forms 认证的控制方式一模一样。

[Authorize] 标记:

表示必须拥有 Claim 的 Cookie 才能访问,否则就会跳转到指定的页面,我们一般指定跳转到登录页面。可以放到 Controller 或 Action 上。如果该标记添加到 Controller 上,则该 Controller 下的所有 Action 都会验证是否拥有 Claim 的 Cookie。

[AllowAnonymous] 标记:

表示允许匿名用户访问,没有登录也可以访问,也就是不需要 Claim 的 Cookie 就可以访问。可以放到 Controller 或 Action 上。通常的使用场景是:在 Controller 上放了 [Authorize] 之后,在该 Controller 下具体的一小部分 Action 上放 [AllowAnonymous] 标记。这样就保证 Controller 下只有一小部分 Action 允许未登录用户访问,其它的 Action 必须用户登录后才能访问。


5 获取 Claim 的 Cookie 中的信息以及退出登录

在上面第 3 步中,我们通过 GenericIdentity gi = new GenericIdentity(name) 这行代码存储登录的用户名,因此我们可以通过 User.Identity.Name 获取到登录的用户名。

下面列出获取 Cookie 中存储的其它信息,比如上面存储的 Power 和 Role

[Authorize]
public IActionResult Display()
{
    StringBuilder sb = new StringBuilder();

    // 遍历获取所有的存储信息
    foreach (var cl in HttpContext.User.Claims)
    {
        sb.AppendLine($"{cl.Type}|{cl.Value}");
    }

    // 通过便捷的方式获取登录的用户名
    sb.AppendLine(User.Identity.Name);

    // 想要获取到具体的一项存储信息,比如仅仅想获取权限码和角色
    sb.AppendLine(User.FindFirstValue("Power"));
    sb.AppendLine(User.FindFirstValue("Role"));

    return Content(sb.ToString());
}

如果想要用户退出登录,销毁 Cookie 的话,可以采用以下代码:

public IActionResult LogOut()
{
    if (User.Identity.IsAuthenticated)
    {
        Task.Run(async () => { await HttpContext.SignOutAsync(); }).Wait();
    }

    //销毁 Claim 的 Cookie 后,可以跳转到登录页面
    return RedirectToAction("Index", "Web");
}

6 优缺点闲谈

到此为止,ASP.NET Core 网站使用 Claim 认证,实现用户登录,访问时进行身份验证方案,已经介绍完了。如果你想要快速搭建轻量级网站应用的话,使用 Claim 认证方式是一种非常方便快速的方案。

但是不建议在大中型项目中使用,还是采用主流的 token + redis 的方案实现用户登录认证比较好,原因如下:

  • 后续项目肯定会涉及到各个系统之间的统一认证对接,以及与第三方的单点登录对接,这种情景使用 token + redis 的方案比较灵活简单,采用 Cookie 的实现方式比较麻烦。
  • 对于用户请求负载均衡分发的场景,token + redis 的方案是非常好的方案,因为其天然保证所有请求都是无状态的,不需要在负载均衡服务器上配置会话保持,这样负载均衡服务器就可以根据每台应用服务器的负载状况,随意使用任何负载请求分发策略。
  • 对于负载均衡来说, Claim 认证方案就不够灵活了,因为其 Cookie 只能在具体的服务器上进行加解密,从而识别登录用户。如果新的请求被负载均衡服务器分发到另一台应用服务器的话,那么就无法解密 Cookie 从而导致需要频繁重新登录,无法使用网站。只有在负载均衡服务器上配置会话保持,从而实现在一定的时间内将来自同一 ip 的请求分发到固定的应用服务器上,才能解决问题。(具体在各个不同的服务器之间,如何实现 Claim 的 Cookie 采用统一的加解密算法,我个人没有研究过。其实也没必要研究了,因为我们大部分情况下都会使用目前比较流行的 token + redis 方案,能够轻松解决任何问题)