从壹开始 [ Ids4实战 ] 之三║ 详解授权持久化 & 用户数据迁移
回顾
哈喽大家周三好,今天终于又重新开启 IdentityServer4 的落地教程了,不多说,既然开始了,就要努力做好😸。
书接上文,在很久之前的上篇文章《二║ 基础知识集合 & 项目搭建一》中,我们简单的说了说 IdentityServer4 是如何调用和配置 Token 的,主要是一些入门的基础知识和概念,也算是第一次尝鲜了,其实 Ids4 本身没有那么神秘,我们只需要知道它是一个丰富的认证框架,又具有很好的可扩展性,核心就是如何配置某些客户端通过该授权服务来包括另一些资源服务器,供用户使用。因为中间隔了很长时间了,这里简单的回顾下上篇文章说了什么,下边这几个小问题,相信大家脑中都能很快的有一些概念,至少是脸熟了,如果不是很清楚,请翻看上边的链接,或者官网再看看:
1、什么是资源?什么是客户端?什么是令牌 ?
2、IdentityServer4 主要是授权还是认证 ?
3、OAuth 2.0、OpenID、OpenID Connect三者的关系 ?
4、Ids4 依赖的 Nuget 包是什么 ?
5、如何快速开发 UI ?
今天这篇文章主要是操作和探索相结合的一篇,我看网上很多教程,基本上是按照官网概念的知识点,配合着 Github 开源地址,讲解一遍,知识是懂了,可是要是自己开发起来,还是有很多的疑惑,甚至是根本走不下去,我就从初学者的立场出发,换一个思路,简单的对其进行探索,希望能让初学者感兴趣,而不会失去学习的冲动,当然,这篇文章不会一下子就很完整,需要一段时间的沉淀和完善,我会不定时将学习到的感悟一一补充下来,然后也会和别人讨论的心得写下来,慢慢的将这篇文章逐渐完整起来。希望大家都积极评论,混个脸熟也会挺好,如果一直潜水,可能都不知道你的存在。😂
之前的配置都是在内存中,下面将如何把这些数据存储到Sql Server数据库, 这样更符合生产环境的要求。好啦,马上开始今天的内容~~~
(这是简单处理了下登录页,要用就要好看些)
一、基于 EFCore 的持久化操作
1、安装依赖环境 ——基础
我们要把所有的数据都存储下来,采用接口进行建模,我们在 IdentityServer4.EntityFramework
Nuget 包中提供这些接口的 EF 实现。
算上我们上篇文章中引用的 IdentityServer4 包,一共这三个 Nuget 依赖环境(具体的版本,以我的项目为准):
<PackageReference Include="IdentityServer4" Version="3.1.3" /> <PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" /> <PackageReference Include="IdentityServer4.EntityFramework" Version="3.1.3" />
2、三个上下文 —— 核心
这一节大家如果看懂的话,一定会对 IdentityServer4 丰富性和可扩展性 有特别切身的了解和体会!😁
Z、为什么要定义多个上下文?
为什么要多个上下文?
还记得在第二个系列 DDD 中,我们说到了多个上下文的用途,单个数据库可以有多个上下文。例如, 如果您的数据库包含多个数据库架构, 并且您希望将每个架构作为单独的自包含区域处理, 则此功能非常有用。在使用多个上下文类型时,您还会看到其他问题, 例如共享实体类型及其从一个上下文传递到另一个上下文等。一般来说,设计多个上下文,会使您的设计更干净, 并分离不同的功能区域,但同时它的成本也增加了复杂性。框架之所以会设计成多个上下文,是为了更好的扩展,我们也可以部署到不同的物理机数据库中,更好的实现独立性。
在我们的 IdentityServer4-EF 框架中,有三个上下文,准确来说是 2+1 个上下文,为什么这么说呢 ?
因为第三个上下文不是官方定义的,是我们自己根据需要进行合理扩展的,比如我们可以使用 Asp.Net Core 自带的 Identity 身份认证机制来实现扩展,当然,你也可以自己定义相应的操作,如果你还不懂,就想象一下我们在 Blog.Core 项目中,我们不是自己定义的基于 JWT 的认证授权么( 用户表 + 角色表 + 关系表等等 ),但是如果你看我的 DDD 项目的话,应该可以看到我又使用了 Core 官方自带的 Identity 机制,毕竟结合 EFCore 我们可以很快的实现用户数据的管理,而不用自己造轮子了。
咱们这里先说说前两个上下文,在我们使用 Ids4 的时候,有两种类型的数据需要持久化到数据库中:
1、配置数据(资源、客户端、身份);//这里是对应配置上下文
ConfigurationDbContext
2、IdentityServer在使用时产生的 操作数据(令牌,代码和用户的授权信息consents);//这里是对应操作上下文
PersistedGrantDbContext
这两个上下文以及对应的数据模型,已经被 IdentityServer4 官方给封装好了, 我们不需要做额外的操作,直接进行迁移即可。
A:ConfigurationDb
ConfigurationDbContext (IdentityServer configuration data) —— 负责数据库中对客户端、资源和 CORS 设置的配置存储;
如果需要从 EF 支持的数据库加载客户端、标识资源、API 资源或 CORS 数据 (而不是使用内存中配置), 则可以使用配置存储。此支持提供 IClientStore、IResura Store 和 ICorsPolicyService 扩展性点的实现。这些实现使用名为 ConfigurationDbContext 的 dbcontext 派生类对数据库中的表进行建模。
具体在 startup 中如何配置,下面会说到。
B:PersistedGrantDb
PersistedGrantDbContext (IdentityServer operational data.) -—— 负责存储同意、授权代码、刷新令牌和引用令牌;
如果需要从 EF 支持的数据库 (而不是默认的内存数据库) 加载授权授予、同意和令牌 (刷新和引用), 则可以使用操作存储。此支持提供了 IPersistedGrantStore 扩展点的实现。实现使用名为 PersistedGrantDbContext 的 dbcontext 派生类对数据库中的表进行建模。
C:ApplicationDb
ApplicationDbContext 继承 IdentityDbContext (Entity Framework database context used for identity.) —— 负责与 asp. net 标识相关的用户;
这个上下文,就是 IdentityServer4 体现 可扩展性 的关键点,为什么说是可扩展的,因为这是我们自己定义的,你可以在这里仅仅定义一个User表,也可以像我们的 Blog.Core 项目中,定义三个表。我们可以在这个上下文中,进行配置,来控制我们的用户数据,当然这里看着很简单,是因为继承了 NetCore 官方的 Identity 了,可以使用他们的那一套逻辑。
public class ApplicationUser : IdentityUser { //可以在这里扩展,下文会说到 } // 定义用户管理上下文,继承 NetCore 自带的 Identity 认证机制,也可以不继承而自定义表结构。 public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Customize the ASP.NET Identity model and override the defaults if needed. // For example, you can rename the ASP.NET Identity table names and more. // Add your customizations after calling base.OnModelCreating(builder); } }
这个上下文,主要是用来处理我们的用户数据相关的部分:
1、有多少用户数据和角色数据;
2、哪些用户,又拥有什么角色,又对应哪些Claims 声明。
这一块更像是我们的平时的权限管理系统,就是在 User、Role、Claim 之间交互,文章下边会有一个数据模型图,会很清晰的看出来各自的对应关系。
我们既然需要用到这几个上下文,就需要添加他们所对应的服务,别着急,往下看。
3、配置 EF-Ids4 相关服务 —— 必要
上边我们说到了三个上下文,如果我们直接执行迁移命令是会报错的,比如我们直接迁移 PersistedGrantDbContext 上下文:
所以,就需要在项目中配置对应的服务,我们在 startup.cs 启动文件中,配置服务 ConfigureService :
配置 EF 操作数据库,很简单,这个就按照平时我们使用的正常的方法即可:
1、获取数据库连接字符串:
( 我这里使用的是读取文件的形式,其实就是字符串放到文件里了,大家自己根据需要做相应调整即可,我这么是防止密码泄露。)
2、配置数据库服务:
var builder = services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; })
// Configures IdentityServer to use the ASP.NET Identity implementations of IUserClaimsPrincipalFactory,
// IResourceOwnerPasswordValidator, and IProfileService. Also configures some of
// ASP.NET Identity's options for use with IdentityServer (such as claim types to
// use and authenticaiton cookie settings).
.AddAspNetIdentity<ApplicationUser>() // 添加配置数据(客户端 和 资源) .AddConfigurationStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); }) // 添加操作数据 (codes, tokens, consents) .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); // 自动清理 token ,可选 options.EnableTokenCleanup = true; });
然后是我们的用户数据存储,其实我们可以根据自己的需要,自己建立对应的表,比如我们 Blog.Core 项目中,我们定义了用户、角色和关系表等数据库接口,但是这样的话,可能少一些情况,所以一般情况下,都是采用的 asp.net core 自带的 Identity 身份验证,而且对扩展性很友好,就比如下文我们就对用户主表做了自定义扩展,继承了 IdentityUser, ApplicationUser : IdentityUser
// 数据库配置系统应用用户数据上下文
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); // 启用 Identity 服务 添加指定的用户和角色类型的默认标识系统配置 services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();
4、Migrations 迁移 —— 结果
我这里提供了两种方法,至于哪种更好,大家可以自己都试试,我都用过,可能第二种稍微多了一点,不过原理都是一样的。
方式一:在包控制台
一、迁移项目( 使用 Package Manager Console ):
1、add-migration InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
2、add-migration InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
3、add-migration AppDbMigration -c ApplicationDbContext -o Data
4、update-database -c PersistedGrantDbContext
5、update-database -c ConfigurationDbContext
6、update-database -c ApplicationDbContext
最后成功的生成了我们需要的数据库:
方式二:在命令窗口
判断是否支持命令行迁移,你可以在项目所在的目录下打开一个命令 Power shell 并运行命令 dotnet ef, 它应该是这样的:
我是在 PowerShell 中操作的:
//1.dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb //2.dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb //3.dotnet ef migrations add AppDbMigration -c ApplicationDbContext -o Data //4.dotnet run /seed
这里我就不一一操作了,就是普通的命令执行,生成的数据库一模一样的。
但是要注意下,上边我们在包管理控制台,仅仅是迁移了数据库,还没有进行数据 Seed 生成,不过你看 PowerShell 的命令中的第四步,我使用 dotnet run /seed 的命令,和上边的不一样,没错,是因为下边我用了另一种方法,通过代码的方法进行 Migrate 了,并且同时还 Seed 了数据,那具体如何操作呢,别慌,第三节我会说到了,现在这个时候,我们已经把数据库的所有表都已经生成好了,先不着急同步数据,我们先看看到底生成了哪些数据表。
二、数据库表的含义(更新中)
1、整体数据模型图
我简单的做了下整个数据库的数据模型图,不同模块用不同的颜色区分,大家也能很清楚的看出来各自对应的关系,一目了然,其中配置数据又有三个部分,分别对应的是 客户端 + 资源 + 身份 。
接下来,咱们针对这三个模块简单的说一说,因为这里还没有数据,说起来可能会比较抽象,这里先留一个坑,等配合 Vue 客户端使用了以后,再把完整数据展示出来,可能更好的理解了。
2、三个模块表
操作数据模块
这是 Ids4 官方封装的第一部分——操作数据。PersistedGrants的数据,在登陆时当你同意请求许可的时候,就会在这个表里面添加一条数据。
举个栗子,我在 Blog.Vue 项目点击登录,跳转到 BlogIdp 授权中心,认证成功后,跳转回来,就会在这个表中多了一条数据(这个功能我下次会开放出来,代码已经写好):
MMD4TtLkVH7sXIwFS/DLZRfLQnUga0XMrBDVuQZWfOw= | user_consent | c2841087-1b35-4ab4-afd6-f27f80c94e6c | blogvuejs | 2019-05-08 03:35:30.0000000 | NULL | {"SubjectId":"c2841087-1b35-4ab4-afd6-f27f80c94e6c","ClientId":"blogvuejs","Scopes":["openid","profile","roles","blog.core.api"],"CreationTime":"2019-05-08T03:35:30Z","Expiration":null} |
配置数据模块
这一大块,是 Ids4 官方定义的第二模块 —— 配置数据,从这里我们可以看出来 IdentityServer4 的另一个特性——丰富性,上边咱们说到了可扩展性,它提供了丰富的配置数据结构,从三个方面来给我们定义好了这些数据,而不用我们再去一个一个的去设计,去思考。
我们可以通过这些表结构来存储我们的服务数据,比如有哪些API资源(Blog.Core.API),哪些客户端资源,还有用到了哪些资源等等,如果你还不清楚的,可以想想上篇文章中,我们讲内存模式的 Ids4 的配置,就是这些数据,在下边的文章中,我们会进行 Seed Data,那个时候我们再进一步看下。
具体的数据大家可以看看,截个图:
用户数据模块
这一块我们就很清楚了,因为这是我们自定义的用户数据(当然本项目是直接继承的 NetCore 的 Identity) ,光从数据库表名上,我们就知道其中的含义了,就是用户角色管理。
下边咱们会将 Blog.Core 项目中的用户信息导进来,先看看 AspNetUsers 表中是怎样的数据:
上边咱们也简单的说了下数据库表结构,然后再结合三个上下文,大家应该都能明白各自的含义了,好啦,现在就是万事俱备,只欠数据了,是时候动手了!
三、同步更新其他项目的用户数据
还记得我们上边在生成数据库的时候,我采用了两个方法,第一种很常规,那接下来就说说第二种中第四步( dotnet run /see ),是如何操作的。
1、定义 SeedData 类
首先,需要定义一个 SeedData 类,主要是用来 update-database 和 Seed Data 这两个作用,因为篇幅的原因,具体的代码在 Github 上,大家自行下载查看即可:
2、扩展用户主表数据
上边咱们也说到了,我们通过继承使用 Identity 机制,来扩展了用户管理,很自然的,我们需要用到 IdentityUser ,但是它的这个用户数据肯定不能满足我们的需求,所以,这里又一次体现了 Ids4 可扩展性的优点,我们可以自定义用户主表:
// Add profile data for application users by adding properties to the ApplicationUser class public class ApplicationUser : IdentityUser { // 自定义属性 public string name { get; set; } public string RealName { get; set; } public int sex { get; set; } = 0; public int age { get; set; } public DateTime birth { get; set; } = DateTime.Now; public string addr { get; set; } public bool tdIsDelete { get; set; } }
3、获取 Blog 项目用户数据
这个功能是一个充分不必要的条件,主要是为了解决某些项目先用了自身一套认证系统,然后又上了 IdentityServer4 项目的情况,就比如我们的 Blog.Core 项目,就是这个情况。
那用户的数据从哪里拿到呢,很巧,大家还记得我在某一篇文章《 42 ║支持多种数据库 & 快速数据库生成》中,自动生成数据库的方案,就是这么使用的,从 Github 上直接拉取数据即可,这里整个派上用场,感觉很不错,
private static string GitJsonFileFormat = "https://github.com/anjoy8/Blog.Data.Share/raw/master/BlogCore.Data.json/{0}.tsv"; // 远程获取 Blog.Core 数据库的三表数据 var BlogCore_Users = JsonHelper.ParseFormByJson<List<sysUserInfo>>(GetNetData.Get(string.Format(GitJsonFileFormat, "sysUserInfo"))); var BlogCore_Roles = JsonHelper.ParseFormByJson<List<Role>>(GetNetData.Get(string.Format(GitJsonFileFormat, "Role"))); var BlogCore_UserRoles = JsonHelper.ParseFormByJson<List<UserRole>>(GetNetData.Get(string.Format(GitJsonFileFormat, "UserRole")));
4、重新定义配置类 Config.cs
PS:现在Ids4的版本已经是4.x系列了,会有部分差异,具体更新,可以看我的另一篇文章
https://mp.weixin.qq.com/s/C1JTz15pBkx4uUa_6qN_eA
咱们在上篇文章中,定义了一个 InMemoryConfig 基于内存的配置文件,咱们再进行完善下,应用到真实项目 Blog.Vue 中:
public class Config { // scopes define the resources in your system public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource("roles", "角色", new List<string> { JwtClaimTypes.Role }), }; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("blog.core.api", "Blog.Core API") { // include the following using claims in access token (in addition to subject id) //requires using using IdentityModel; UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.Role } } }; } // clients want to access resources (aka scopes) public static IEnumerable<Client> GetClients() { // javascript client return new List<Client> { new Client { ClientId = "blogvuejs", ClientName = "Blog.Vue JavaScript Client", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = { "http://localhost:6688/callback" }, PostLogoutRedirectUris = { "http://localhost:6688" }, AllowedCorsOrigins = { "http://localhost:6688" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "roles", "blog.core.api" } } }; } }
5、执行Seed方法,查看数据
经历了定义上下文 -> 数据库迁移 -> 获取数据 -> Seed Data 的过程后,就剩下最后一步了,执行操作:
很简单,我在 Blog.Core 项目中,采用的是 appsettings.json 文件配置的方法,这里换另一种方法,Program.cs 控制台参数的形式
四、美化部分页面
在上一讲中,我们用到了官方的 快速启动 代码,里边已经包含了基本的页面,当然你也可以根据自己的情况去自定义,甚至采用静态的Ajax 来实现,我这里还是使用的 MVC 结构,只不过套用了下一个简单样式(样式版权是http://www.uimaker.com/ 网站的,请不要做商业用途,注意产权),至少看起来好看了一些。
具体的登录操作就不演示了,等下次把 增删改查 都做好了,再统一做下动图展示吧,具体如何设计,请听下回分解。
五、结语
今天我们简单的实现了对 IdentityServer4-EFCore 的相关配置,概念不是很难,为了加深印象,还是提几个小问题,请大家多思考:
1、Ids4 一共用到了几个上下文,分别的用处是什么?
2、在迁移中,数据库生成了多少表,各个模块又是干什么的?
3、Ids4 的良好扩展性,体现在哪里?丰富性又体现在哪里?
4、ApplicationUser 类是起到什么作用的?
5、思考1:如果同一个用户有多个角色如何处理?
6、思考2:如果不使用 NetCore 自带的 Identity 认证,如何自定义上下文?
六、Github
https://github.com/anjoy8/Blog.IdentityServer
---♥---♥--- ---♥---♥--- ---♥---♥---