在 ASP.NET Core 应用中使用 Cookie 进行身份认证

Overview

身份认证是网站最基本的功能,最近因为业务部门的一个需求,需要对一个已经存在很久的小工具网站进行改造,因为在逐步的将一些离散的系统迁移至 .NET Core,所以趁这个机会将这个老的 .NET Framework 4.0 的项目进行升级

老的项目是一个 MVC 的项目并且有外网访问的需求,大部门的微服务平台因为和内部的业务执行比较密切,介于资安要求与外网进行了隔离,因此本次升级就不会迁移到该平台上进行前后端分离改造

使用频次不高,不存在高并发,实现周期短,所以就没有必要为了用某些组件而用,因此这里还是选择沿用 MVC 框架,对于网站的身份认证则采用单体应用最常见的 Cookie 认证来实现,本篇文章则是如何实现的一个基础的教程,仅供参考

Step by Step

在涉及到系统权限管理的相关内容时,必定会提到两个长的很像的单词,authentication(认证) 和 authorization(授权)

  • authentication:用一些数据来证明你就是你,登录系统、指纹、面部解锁就是一种认证的过程
  • authorization:授予一些用户去访问一些特殊资源或功能的过程,系统包含管理员和普通用户两种角色,只有管理员才可以执行某些操作,赋予管理员角色某些操作的过程就是授权

只有认证和授权一起配合,才可以完成对于整个系统的权限管控

2.1、前期准备

假定现在已经存在了一个 ASP.NET Core MVC 应用,这里以 VS 创建的默认项目为例,对于一个 MVC or Web API 应用,要求用户必须登录之后才能进行访问,最简单的方式,在需要认证的 Controller 或 Action 上添加 Authorize 特性,然后在 Startup.Configure 方法中通过 UseAuthorization 添加中间件即可

[Authorize]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}
public class Startup
{
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                "default",
                "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

当然,当系统只包含一个两个 Controller 时还好,当系统比较复杂的时候,再一个个的添加 Authorize 特性就比较麻烦了,因此这里我们可以通过在 Startup.ConfigureServices 中添加全局的 AuthorizeFilter 过滤器,实现对于全局的认证管控

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews()
            .AddMvcOptions(options => { options.Filters.Add(new AuthorizeFilter()); });

    }
}

此时,对于一些不需要进行认证就可以访问的页面,只需要添加 AllowAnonymous 特性即可

public class AuthenticationController : Controller
{
    [AllowAnonymous]
    public IActionResult Login()
    {
        return View();
    }
}

2.2、配置认证策略

当然,如果只是这样修改的话,其实是有问题的,可以看到,当添加上全局过滤器后,系统已经无法正常的进行访问

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

对于 authorization(授权) 来说,它其实是在 authentication(认证)通过之后才会进行的操作,也就是说这里我们缺少了对于系统认证的配置,依据报错信息的提示,我们首先需要通过使用 AddAuthentication 方法来定义系统的认证策略

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

AddAuthentication 方法位于 Microsoft.AspNetCore.Authentication 类库中,通过在 Nuget 中搜索就可以发现,.NET Core 已经基于业界通用的规范实现了多个认证策略

因为这里使用的 Cookie 认证已经包含在默认的项目模板中了,所以就不需要再引用了

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

基于 .NET Core 标准的服务使用流程,首先,我们需要在 Startup.ConfigureServices 方法来中通过 AddAuthentication 来定义整个系统所使用的一个授权策略,以及,基于我们采用 Cookie 授权的方式,结合目前互联网针对跨站点请求伪造 (CSRF) 攻击的防范要求,我们需要对网站的 Cookie 进行一些设定

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 定义授权策略
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                // 无权访问的页路径
                options.AccessDeniedPath = new PathString("/permission/forbidden");

                // 登录路径
                options.LoginPath = new PathString("/authentication/login");

                // 登出路径
                options.LogoutPath = new PathString("/authentication/logout");

                // Cookie 过期时间(20 分钟)
                options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
            });

        // 配置 Cookie 策略
        services.Configure<CookiePolicyOptions>(options =>
        {
            // 默认用户同意非必要的 Cookie
            options.CheckConsentNeeded = context => true;

            // 定义 SameSite 策略,Cookies允许与*导航一起发送
            options.MinimumSameSitePolicy = SameSiteMode.Lax;
        });
    }
}

如代码所示,在定义授权策略时,我们定义了三个重定向的页面,去告诉 Cookie 授权策略这里对应的页面在何处,同时,因为身份验证 Cookie 的默认过期时间会持续到关闭浏览器为止,也就是说,只要用户不点击退出按钮并且不关闭浏览器,用户会一直处于已经登录的状态,所以这里我们设定 20 分钟的过期时间,避免一些不必要的风险

至此,对于 Cookie 认证策略的配置就完成了,现在就可以在 Startup.Configure 方法中添加 UseAuthentication 中间件到 HTTP 管道中,实现对于网站认证的启用,这里需要注意,因为是先认证再授权,所以中间件的添加顺序不可以颠倒

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

        // 添加认证授权(顺序不可以颠倒)
        //
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                "default",
                "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

此时,当我们再次访问系统时,因为没有经过认证,自动触发了重定向到系统登录页面的操作,而这里重定向跳转的页面就是上文代码中配置的 LoginPath 的属性值

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

2.3、登录、登出实现

当认证策略配置完成之后,就可以基于选择的策略来进行登录功能的实现。这里的登录页面上的按钮,模拟了一个登录表单提交,当点击之后会触发系统的认证逻辑,实现代码如下所示。这里别忘了将登录事件的 Action 上加上 AllowAnonymous 特性从而允许匿名访问

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginAsync()
{
    // 1、Todo:校验账户、密码是否正确,获取需要的用户信息

    // 2、创建用户声明信息
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, "张三"),
        new Claim(ClaimTypes.MobilePhone, "13912345678")
    };

    // 3、创建声明身份证
    var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

    // 4、创建声明身份证的持有者
    var claimPrincipal = new ClaimsPrincipal(claimIdentity);

    // 5、登录
    await HttpContext.SignInAsync(claimPrincipal);

    return Redirect("/");
}

在整块的代码中,涉及到三个主要的对象,ClaimClaimsIdentityClaimsPrincipal,通过对于这三个对象的使用,从而实现将用户登录成功后系统所需的用户信息包含在 Cookie 中

三个对象之间的区别,借用理解ASP.NET Core验证模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不读的英文博文这篇博客的解释来说明

  • Claim:被验证主体特征的一种表述,比如:登录用户名是...,email是...,用户Id是...,其中的“登录用户名”,“email”,“用户Id”就是 ClaimType
  • ClaimsIdentity:一组 claims 构成了一个 identity,具有这些 claims 的 identity 就是 ClaimsIdentity ,驾照就是一种 ClaimsIdentity,可以把 ClaimsIdentity理解为“证件”,驾照是一种证件,护照也是一种证件
  • ClaimsPrincipal:ClaimsIdentity 的持有者就是 ClaimsPrincipal ,一个 ClaimsPrincipal 可以持有多个 ClaimsIdentity,就比如一个人既持有驾照,又持有护照

最后,通过调用 HttpContext.SignInAsync 方法就可以完成登录功能,可以看到,当 Cookie 被清除后,用户也就处于登出的状态了,当然,我们也可以通过手动的调用 HttpContext.SignOutAsync 来实现登出

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

2.4、获取用户信息

对于添加在 Claim 中的信息,我们可以通过指定 ClaimType 的方式获取到,在 View 和 Controller 中,我们可以直接通过下面的方式进行获取,这里使用到的 User 其实就是上文中提到的 ClaimsPrincipal

var userName = User.FindFirst(ClaimTypes.Name)?.Value;

在 ASP.NET Core 应用中使用 Cookie 进行身份认证

而当我们需要在一个独立的类库中获取存储的用户信息时,我们需要进行如下的操作

第一步,在 Startup.ConfigureServices 方法中注入 HttpContextAccessor 服务

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 注入 HttpContext
        services.AddHttpContextAccessor();

    }
}

第二步,在你需要使用的类库中通过 Nuget 引用 Microsoft.AspNetCore.Http,之后就可以在具体的类中通过注入 IHttpContextAccessor 来获取到用户信息,当然,也可以在此处实现登录、登出的方法

namespace Sample.Infrastructure
{
    public interface ICurrentUser
    {
        string UserName { get; }

        Task SignInAsync(ClaimsPrincipal principal);

        Task SignOutAsync();

        Task SignOutAsync(string scheme);
    }

    public class CurrentUser : ICurrentUser
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        private HttpContext HttpContext => _httpContextAccessor.HttpContext;

        public CurrentUser(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public string UserName => HttpContext.User.FindFirst(ClaimTypes.Name)?.Value;

        public Task SignInAsync(ClaimsPrincipal principal) => HttpContext.SignInAsync(principal);

        public Task SignOutAsync() => HttpContext.SignOutAsync();

        public Task SignOutAsync(string scheme) => HttpContext.SignOutAsync(scheme);
    }
}

至此,整块的认证功能就已经实现了,希望对你有所帮助

Reference

  1. SameSite cookies
  2. Work with SameSite cookies in ASP.NET Core
  3. What does the CookieAuthenticationOptions.LogoutPath property do in ASP.NET Core 2.1?
  4. 理解ASP.NET Core验证模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不读的英文博文
  5. Introduction to Authentication with ASP.NET Core
上一篇:ASP.NET Core获取请求完整的Url


下一篇:ASP.NET Core开发之HttpContext