ASP.NET Core 中使用 ASP.NET Core Identity 实现最简单的用户登录
ASP.NET Core 中使用 ASP.NET Core Identity 实现最简单的用户登录

ASP.NET Core 中使用 ASP.NET Core Identity 实现最简单的用户登录

简述

根据 MSDN 上的原话, ASP.NET Core Identity 是一组支持用户界面登录功能的 API. 用户可以创建一个具有存储在标识中的登录信息的帐户, 也可以使用外部登录提供程序. ASP.NET Core Identity 亦具有管理用户、密码、配置文件数据、角色、声明、令牌、电子邮件确认等功能.

目前互联网上关于 ASP.NET Core Identity 的文章, 要么是囫囵个使用 ASP.NET Core + Entity Framework Core 自动创建的用户体系相关表, 要么只讲述了一半. 但实际上有很多情况下各种系统的用户表是已经存在或者需要根据系统结构和习惯自定义的. 因此这里我把 “在不使用 Entity Framework Core” 的前提下把 整个登录 API 的构建过程从 Action 到 存储 都讲一下.

这里以 ASP.NET Core 3.1 为准.

创建项目

我们打开 Visual Studio 2019, 这里选择创建 ASP.NET Core Web 应用程序, 项目模版选择 API.

创建好之后会发现项目模版中自带了一个示例 Controller. 我们可以拿来参照着写. 当然也可以删掉.

这里主要关注的是 Startup.cs 中的内容. 创建好后内容如下:

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		if (env.IsDevelopment())
			app.UseDeveloperExceptionPage();

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

		app.UseEndpoints(endpoints =>
		{
			endpoints.MapControllers();
		});
	}
}

这时候我们就可以正式开始着手修改了.

修改 Startup.cs

首先我们要明白我们到底要干什么.

我们需要做的是两件事情:

  1. 验证试图登录的信息的正确性.
  2. 对 API 的访问请求进行权鉴.

第一种叫做 “Authentication”. 而第二种叫做 “Authorization”. ASP.NET Core 默认的 Startup.Configure() 函数中已经调用了 "UseAuthorization()" 但是这远远不够. 我们还需要在 Startup.ConfigureServices() 中定义用户登录的方式. 没有登录, 授权就就无从谈起.

而登陆的过程仔细算下来还分为两步: 首先是身份识别 (Identification), 然后是签发 (Issue). 用大白话说就是登录, 登陆成功后下发访问凭证 (通行证). 而在 ASP.NET Core Identity 中, 这两步是通过 “AddIdentity() / AddIdentityCore()” 和 “AddAuthentication()” 实现的. 下面是一个例子:

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();

		services.AddIdentityCore<Employee>()
		.AddUserStore<AccountManager>()
		.AddSignInManager<SignInManager<Employee>>()
		.AddUserManager<UserManager<Employee>>();

		services.AddAuthentication(opt =>
		{
			opt.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
		})
		.AddCookie(IdentityConstants.ApplicationScheme, options =>
		{
			options.LoginPath = "/api/account/forbidden";
			options.AccessDeniedPath = "/api/account/forbidden";
		});
	}

	......
	......
	......
}

这里的 “Employee” 就是一个普通的, Entity Framework Core 生成的 “用户” 实体. 指代 “一个员工”. 定义如下:

public partial class Employee
{
	public int Id { get; set; }
	public string UserName { get; set; }
	public sbyte? Gender { get; set; }
	public string Password { get; set; }
	public sbyte? Status { get; set; }
}

AccountManager” 是一个用户管理的类. 用于接管用户增删改查的功能. ASP.NET Core Identity 中想要实现用户登录就必须提供一个用于验证用户信息的 “用户信息读写器”. 这个类必须实现 IUserStore<用户实体>IUserPasswordStore<用户实体>, 接口, 用以接管存取数据库中用户资料以及密码的逻辑.

.AddUserManager<UserManager<用户实体>>() 实际上算是指定了 “AccountManager” 的访问器. 使用默认的 UserManager 来访问 “AccountManager” 并在此基础上实现用户资料修改, 密码重置等逻辑. 通俗的讲, IUserStore 直接面对从数据库里出来的最原始的用户数据, UserManager 则是聚合用户数据的各种操作, 形成用户管理的逻辑 (改密码, 建用户, 等等…).

.AddSignInManager<SignInManager<用户实体>>() 的职责则简单得多, 就只负责登录的逻辑. 用户名/密码模式登录, 双因子验证登录等…. 其他逻辑不管.

注意, SignInManager<T> 默认使用 Cookie 存储授权后生成的 Token (通行证), 而且不能更改.

.AddAuthentication() 中指定了默认的身份验证方式. 因为有时候可能存在多种身份验证方式, 就像手机可以指纹解锁也可以面部识别解锁一样. 这里我们只有一种, 写死成 IdentityConstants.ApplicationScheme 即可.

注意: AddAuthentication(opt) => opt.DefaultAuthenticateScheme 跟后面 .AddCookie() 的第一个参数一定要一致.

以下是 AccountManager 的一个简易的实现.

/// <summary>
/// 账户
/// </summary>
public class AccountManager : IUserStore<Employee>, IUserPasswordStore<Employee>
{
	private SampleDBContext dbContext = null;
	private IPasswordHasher<Employee> pwdHasher = null;

	public AccountManager(SampleDBContext context, IPasswordHasher<Employee> pwdHasher)
	{
		this.dbContext = context;
		this.pwdHasher = pwdHasher;
	}

	/// <summary>
	/// 根据用户名查询用户
	/// </summary>
	public async Task<Employee> FindByNameAsync(string normalizedUserName,
           CancellationToken cancellationToken)
	{
		return await Task.Run<Employee>(() =>
		{
  			//这里你甚至可以直接写 SQL 来查询你自己的用户表
			var foundUser = dbContext
	                      .Employee
	                      .FirstOrDefault(p => 
	                          p.UserName.ToUpper() == normalizedUserName
	                          && p.Status == 1);

			return foundUser;
		}, cancellationToken);
		
	}

	/// <summary>
	/// 获取用户的密码哈希值
	/// </summary>
	public async Task<string> GetPasswordHashAsync(Employee user,
	    CancellationToken cancellationToken)
	{
		string hashedPassword = pwdHasher.HashPassword(user, user.Password);
		
		return await Task.FromResult(hashedPassword);
	}

	/// <summary>
	/// 读取用户ID
	/// </summary>
	public async Task<string> GetUserIdAsync(Employee user	    
            CancellationToken cancellationToken)
	{
		return await Task.FromResult("" + user.Id);
	}

	/// <summary>
	/// 读取用户名
	/// </summary>
	public async Task<string> GetUserNameAsync(Employee user
	    CancellationToken cancellationToken)
	{
		return await Task.FromResult(user.UserName);
	}

        //
        // 还有一大串其他的各种函数, 这里为了节省篇幅就不列出来了.
        //
}

正常来说这里不会只有5个函数. 各种用户信息增删改查都会自动生成函数让你来实现. 这里我们只是为了登录, 没有其他的用户增删改的功能, 因此其他函数暂且忽略. 只需要实现:

  • FindByNameAsync
  • GetPasswordHashAsync
  • GetUserIdAsync
  • GetUserNameAsync

这四个函数就足够了. 需要注意的是, GetPasswordHashAsync 函数中不能直接返回密码. 而是要经过 PasswordHasher<T> 来对既有的密码进行一次哈希运算后再返回. 直接返回密码或者用其他方式计算密码哈希值会导致无限密码错误. 构造函数中注入的 SampleDBContext是 Entity Framework Core 的 DBContext. 这里你可以选用任何一个数据库访问中间件, Dapper, EFCore, 什么都可以…

Controller

到这里登录的配置就可以了. 下面要做的就是 Controller 里增加登录验证的接口了. 话不多说, 上代码.

[ApiController]
[Route("api/[controller]/[action]")]
public class AccountController : ControllerBase
{
	private SignInManager<Employee> loginManager;
	private UserManager<Employee> userManager;

	public AccountController(UserManager<Employee> userManager, SignInManager<Employee> loginManager)
	{
		this.userManager = userManager;
		this.loginManager = loginManager;
	}

	/// <summary>
	/// 登录
	/// </summary>
	[HttpPost]
	public async Task<DataResponse<Employee>> Login(LoginRequest req)
	{
		if (!ModelState.IsValid)
			return DataResponse<Employee>.Fail("登录信息不完整");

		var user = await userManager.FindByNameAsync(req.UserName);

		if (user == null)
			return DataResponse<Employee>.Fail("用户不存在");

		//我们用户数据库里存储的密码是 MD5 加密的. 因此此处页面传来的密码也需要进行一次 MD5 加密.
		//当然, 如果你头铁的话你也可以选择在数据库里存明文密码
		var hashed = MD5Encrypt(req.Password);

		var result = await loginManager.PasswordSignInAsync(user.UserName, hashed, false, false);

		//不能把用户的密码也传回去啊!
		user.Password = null;

		if (result.Succeeded)
			return DataResponse<Employee>.Success(user, "登录成功");
		else
			return DataResponse<Employee>.Fail("密码不正确");
	}

	/// <summary>
	/// 登陆过期/未登录统一跳转到此 Action 即可.
	/// 这个就是上面看到那个
	/// .AddCookie(Identi... options=>
	///     options.LoginPath = "/api/account/forbidden"
	///  那个 action 的本尊
	/// </summary>
	[HttpGet]
	public async Task<DataResponse<string>> Forbidden(string returnUrl)
	{
		return await Task.FromResult(new DataResponse<string>()
		{
			Data =  returnUrl,
			ResponseCode = 2,
			ResponseMessage = "未登录或者没有访问权限"
		});
	}

	/// <summary>
	/// MD5 加密
	/// </summary>
	private static string MD5Encrypt(string data)
	{
		MD5 md5 = MD5.Create();

		byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(data));

		return string.Concat(hash.Select(p => p.ToString("x2").ToUpper()));
	}

}

LoginRequest 里只有 UserName 和 Password 俩属性.

测试

那么我们现在就可以来试一下了:

最关键的是 Cookie. 因为里面存储着身份验证的信息.

这里返回头中的 Set-Cookie 的内容会被浏览器自动写入 Cookie 之中, 并携带在之后的每一次请求当中…

运用

身份验证的整个过程都已经做完了. 那么之后所有的 Action 被访问的时候都会自动进行身份验证么? 不. 事实上只有加了 [Authorize] 这个 Attribute 的对象才能验证访客的身份. 不加这个 Attribute 的话, 没有登录也是可以访问的. 这里我们随便建立一个 ApiController, 里面随便写个 Action, 在不登录的情况下看是否能够访问.

    [ApiController]
    //[Authorize(AuthenticationSchemes = "Identity.Application")] 不加试试
    [Route("api/post-login/[action]")]
    public class PostLoginController : ControllerBase
    {
        public object Test()
        {
            return new {A = 1};
        }
    }

Cookie 都删除, 访问:

可以看到, 在没有任何可以证明访客身份的情况下接口返回了内容. 那么我把上面代码中 [Authorize(AuthenticationSchemes = IdentityConstants.ApplicationScheme)] 加回来之后再试呢?

可以看到, 这里一旦加上了 [Authorize] 这个 Attribute, 匿名用户就不再能够无条件的访问这个接口了. 而当我们正常进行登录之后, 再去访问此接口呢?

果然又可以访问了.

注意, [Authorize] 这个 Attribute 默认无参, 但是如果不指定 AuthenticationSchemes, 且 AddAuthentication() 时候也没指定 DefaultChallengeScheme 那么接口访问的时候将会报错. 因此必须指定 AuthenticationSchemes.





System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme
found. The default schemes can be set using either AddAuthentication(string defaultScheme) or
AddAuthentication(Action<AuthenticationOptions> configureOptions).
......
......
......

一条评论

发表回复

您的电子邮箱地址不会被公开。