前言
在上一篇文章中,我们学习了 CookieAuthentication 中间件,本篇的话主要看一下 Identity 本身。
最早2005年 ASP.NET 2.0 的时候开始, Web 应用程序在处理身份验证和授权有了很多的变化,多了比如手机端,平板等,所以那个时候为了适应这种变化就引入了ASP.NET Membership,但是随着时间的发展一些社交网站或者程序聚集了大量的用户,比如Facebook,Twitter,QQ等,这个时候用户希望能够使用他们在这些社交站点身份来登陆当前网站,这样可以免除注册这些琐碎而又必要的操作,用户也不必记住大量的账户密码。
又随着互联网的发展,越来越多的开发者不只是关注具体业务代码的编写,转变为开始关注应用程序代码的单元测试,这已经是开发者关注的核心。所以在2008年,ASP.NET 团队引入了 MVC 框架,这样来帮助开发者很方便的构建单元测试,同时开发者希望他们的 Membership 系统也能够做到这一点。
基于以上,ASP.NET Identity 应运而生。
Identity 要解决的问题
很多开发人员说他们不愿意使用Identity,自己实现要方便的多,OK,那么需求来了?以下就是我针对此次任务给你提出来的需求。
身份系统
- 可以同时被所有的ASP.NET 框架使用(Web MVC,Web Forms,Web Api,SignalR)
- 可以应用于构建 Web, 手机,存储,或者混合应用。
- 可以针对用户资料进行扩展。
持久化
- 默认把用户信息存储在数据库中,可以支持使用EF进行持久化。(可以看到,EF 其实只是Identity的一个功能点而已)
- 可以控制数据库架构,更改表名或者主键的数据类型(int,string)
- 可以使用不同的存储机制(如 NoSQL,DB2等)
单元测试
- 使WEB 应用程序可以进行单元测试,可以针对ASP.NET Identity编写单元测试
角色机制
- 提供角色机制,可以使用不同的角色来进行不同权限的限制,可以轻松的创建角色,向用户添加角色等。
要支持基于Claims
- 需要支持基于 Claims 的身份验证机制,其中用户身份是一组Claims,一组Claims可以比角色拥有更强的表现力,而角色仅仅是一个bool值来表示是不是会员而已。
第三方社交登陆
- 可以很方便的使用第三方登入,比如 Microsoft 账户,Facebook, Twitter,Google等,并且存储用户特定的数据。
封装为中间件
- 基于中间件实现,不要对具体项目产生依赖
- 基于 Authorzation 中间件实现,而不是使用 FormsAuthentication 来存储cookie。
NuGet包提供
- 发布为 Nuget 包,这样可以容易的进行迭代和bug修复,可以灵活的提供给使用者。
以上,就是我提出来的需求,如果让你来封装这样一个用户身份认证组件,你会不是想到以上的这些功能点,那针对于这些功能点你又会怎么样来设计呢?
下面来看一下 Identity 怎么样设计的吧。
Getting Started
抽丝剥茧,我们先从入口看一下其使用方式。 首先我们打开 Startup.cs 文件,然后添加如下代码:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
options.Cookies.ApplicationCookie.AuthenticationScheme = "ApplicationCookie";
options.Cookies.ApplicationCookie.CookieName = "Interop";
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
}
public void Configure(IApplicationBuilder app)
{
// 使用了 CookieAuthentication 中间件做身份认证
app.UseIdentity();
}
}
在 ConfigureServices 中,先是注册了数据库上下文,然后又 services.AddIdentity()
我们看一下里面都注册了哪些服务呢?
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
where TUser : class
where TRole : class
{
// 这个就是被 Identity 使用的
services.AddAuthentication(options =>
{
// This is the Default value for ExternalCookieAuthenticationScheme
options.SignInScheme = new IdentityCookieOptions().ExternalCookieAuthenticationScheme;
});
// 注册 IHttpContextAccessor ,会用到
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Identity services
services.TryAddSingleton<IdentityMarkerService>();
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
// 错误描述信息
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
//身份当事人工厂
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
//三大对象
services.TryAddScoped<UserManager<TUser>, UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>, RoleManager<TRole>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}
看了以上代码后,基本上知道了 Identity 他的设计的一个架构了,通过此结构我们也能够学习到我们自己封装一个中间件的时候,该怎么样来组织我们的代码结构,怎么样的利用 ASP.NET Core 给我们提供的依赖注入来更好的解耦,下面我们来看一下通过以上代码我们能够学到什么东西:
1、 在 public static IdentityBuilder AddIdentity<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction)
这个扩展方法中,提供了一个参数 Action<IdentityOptions>
,这个是干什么用的呢? 这个是我们在设计一个中间件的时候,有需要外部提供的参数,就会设计一个 Options 的类用来接受外部的参数,然后封装为一个Action委托的方式提供。在使用到的地方就可以以 IOption xxx 的形式注入进来使用了。
2、services.TryAddScoped<Interface,Implement>()
,这个注册方式就是说,如果检测到DI容器里面已经有了当前要注册的Interface或者Service,就不会再次注册,没有才会注册进去。 那么为什么此处要这样用呢? 这是因为如果用户已经实现了此接口并且已经注册的容器当中的话,就使用用户注册的,而不是中间件自身的。用户就能很好的对中间件提供的功能进行自定义了,这就是OO中的多态性,这就是里氏替换原则。
3、如果你能理解第2条的话,那么你应该知道为什么会在服务中注册 错误描述信息 那个IdentityErrorDescriber
了吧,也能够解决你想提示账号密码错误,无奈Identity输出是英文问题的提示。
4、三大对象,这个是 Identity 的核心了,所以学习 Identity 的话,在看完博客 ASP.NET Core 之 Identity 入门(一,二)之后,学这三个对象就够了。
SignInManager: 主要处理注册登录相关业务逻辑。
UserManager: 处理用户相关添加删除,修改密码,添加删除角色等。
RoleManager:角色相关添加删除更新等。
有些同学可能很好奇,都没有依赖具体的数据库或者是EF,是怎么样做到的增删改查的呢?
这个时候,就需要几个 Store 接口派上用场了。以下是Identity中定义的Store接口:
- IQueryableRoleStore
- IQueryableUserStore
- IRoleClaimStore
- IRoleStore
- IUserAuthenticationTokenStore
- IUserClaimStore
- IUserEmailStore
- IUserLockoutStore
- IUserLoginStore
- IUserPasswordStore
- IUserPhoneNumberStore
- IUserRoleStore
- IUserSecurityStampStore
- IUserStore
- IUserTwoFactorStore
有了这些接口之后,是不是豁然开朗了,原来 Identity 是通过这种方式实现的持久化机制,依赖抽象接口而不是依赖具体的细节实现,这就是面向对象中的依赖倒置原则呀。
Identity 和 EntityFramework
Identity 和 EntityFramework的关系,相信上个章节看懂了之后,就很容易明白了,对的,EF 只是针对于上述 Store 接口的实现,不信你看截图的源码:
Identity 打头的那些类文件都是定义的需要持久化的Entity对象,Store结尾的那些就是接口的实现啦。
第三方的 Identity 实现
除了 EF 是官方默认提供的持久化库之外,还有一些第三方的库,当然你也可以自己使用 ADO.NET 或者 Drapper 实现。
MangoDb 针对于 Identity 提供的实现: https://github.com/tugberkugurlu/AspNetCore.Identity.MongoDB
LinqToDB 针对于 Identity 提供的实现:https://github.com/linq2db/LinqToDB.Identity
总结
这篇博文写了蛮久的时间的,一方面是因为在构思怎么样的思路来让大家更好的理解,而不仅仅是使用。因为有太多的文章介绍Identity 的使用方式以及代码了,但是最后大家还是不会用。后来想到如果让别人想要理解你的库也好代码也好,让其知道诞生的背景是很重要的,因为这才是设计的初衷。另一方面是因为Connect() 2016 大会上,.NET Core 发布了 1.1 版本,除了把项目升级到1.1之外,也在学习1.1新的一些东西,以便更好给大家分享。
授人以鱼不如授人以渔, 一篇好的博文确实要花费作者更多的心血和时间。如果你觉得这篇博文对您有帮助,感谢您的【推荐】。
同时,ASP.NET Core 之 Identity 系列也结束了,感谢支持的朋友们。如果你对 .NET Core 感兴趣可以关注我,我会定期在博客分享关于 .NET Core 的学习心得。