从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调

前言

哈喽~~~ 大家周一好!夏天到了,大家舒服了没有,熟话说,打败你的不是天真,是天真热!?

过去的一周里,发生了两件事跟大家分享下:

①、有两个小伙伴给我提供了 Working Online 的工作,简单说了说,感觉应该不太适合,至少我不适合,前期双方试探的成分太多了,我是不喜欢,同时也建议正在找OnLine工作的小伙伴需多多考虑可行性。

②、我决定开始《微讲堂》了,具体可以参考右侧公告栏,因为有些基础比较薄弱的小伙伴,单单提供思路还是无法入门,所以提供在线手把手教学吧,这个我是完全无所谓,看你心情吧。

这几天通过晚上对 IdentityServer4 的学习和研究,发现这个就是一个“大坑”(不是说功能不好,是里边有很多很多的内容需要学习),之前看官网, 关于 IdentityServer4 的教程,洋洋洒洒就过去了,感觉还挺简单,发现要真是落地到项目里了,自我感觉又有了压迫感,文末结语中,我简单的说了几点问题,大家可以慢慢往下走,不过知识嘛,无外乎就是自己开心学习 和 自己学习挣钱,这两个心理,加油吧。

当然平时工作之余,还是要照顾下前后端分离项目的一些东西的,基础不能丢,主要是三块地方做了修改,这里简单的列一下,就不单独的写文章了,希望一直在看第一个项目的小伙伴,有缘可以看到吧,不过,就算是看不到也没事儿,遇到了自然就知道了:

1、Blog.Vue 首页的闪屏处理;// 知名博主@张飞洪提出的问题,不知道我是否修改对了;http://core-dotnet.com:8077

2、Blog.Admin 后台框架调整优化;// ①登录页样式改版,②Tabs 导航条优化,③兼容手机屏幕等;http://core-dotnet.com:2364

3、Blog.Core 后端项目增加 Wiki 页;// 为了让刚接触框架的小伙伴能快速一览,特地在 Github 上,创建了 Wiki ,只不过现在才打了个目录,内容慢慢填,如果还有其他的不足之处,欢迎提建议;https://github.com/anjoy8/Blog.Core/wiki

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第1张图片
image

突然转话题,上次咱们第一次对项目进行持久化操作《三║ 详解授权持久化 & 用户数据迁移》,不知道小伙伴都看了多少,这里再把几个重要问题提一下,希望不要忘记了才好:

1、Ids4 一共用到了几个上下文,分别的用处是什么? 2、在迁移中,数据库生成了多少表,各个模块又是干什么的? 3、Ids4 的良好扩展性,体现在哪里?丰富性又体现在哪里? 4、ApplicationUser 类是起到什么作用的?

如果脑子里有些东西,那就恭喜了,如果第一次看,或者完全不知道我在说什么的话,请看上一集,今天会说说我在研究遇到的两个 Flag ?,也就是两个问题,希望有心的小伙伴,可以帮忙思考下,欢迎找我讨论,废话不多说,开车,马上讲解今天的内容!??

零、今天要实现绿色的部分

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第2张图片
原文lw

(知识结构图,注意这是我自己的讲解结构,和Ids4知识图解无关)

一、用户数据处理 —— Identity

咱们在上篇文章中,简单的将 IdentityServer4 的结构进行持久化处理,并把前后端项目中的用户数据进行迁移处理,最后修改了登录页的样式,基本满足了登录和登出的操作,作为一个授权服务中心,仅仅只有登录是完全解决不了什么问题的,至少应该对用户数据进行常规操作处理,比如 CURD 等基础操作。

正好,我们使用了 NetCore 自带的 Identity 机制,可以帮助我们做一部分工作,因为它自己也封装了一些方法,我们可以根据他们的方法,实当的做些扩展,从而达到相应的目的,具体有哪些操作,请往下看:

1、用户数据展示(有权限)

既然有数据处理,肯定得有展示出来,当然,这个不是一定的,只是做下处理,如果你担心会有数据安全问题的话,要么不显示数据,要么只显示无关痛痒的两列,甚至可以直接加上权限,只有超级管理员或者技术人员可以看到就行。我这里仅仅是加了个登录权限,只有登录的用户才能看的到:

// 注入用户管理
private readonly UserManager _userManager;

[HttpGet]
[Route("account/users")]
[Authorize]//可以自定义规则 public IActionResult Users(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl; var users = _userManager.Users.Where(d => !d.tdIsDelete).OrderBy(d => d.UserName).ToList();//Identity 已经对内部的一些方法做了封装,直接使用即可,如果你对 Net 自带的 Identity 使用过的话,应该很容易上手。

 return View(users);

}

注意下上边的红色标注的地方,下文会说到为啥这里用到了 isDelete 。

我们简单的对 User 页面做了授权处理,必须登录状态下才能有权访问,如果是没有登录,会直接跳转到登录页面:

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第3张图片
image

(带权限的用户展示页)

2、注册

关于注册其实我们之前已经说过了,为什么呢,因为我们在之前导入用户数据的时候,就已经用到了这个方法,只不过这里单拎出来了,但是这里有一个问题需要我们好好的思考思考,那就是角色的获取!这里就是我下边要说的第一个“Flag”?,为什么重要呢,不知道现在读的你是否使用过 IdentityServer4 ,我也这几天在考虑这个问题,授权中心肯定需要有用户管理的,那很自然的,就会出现 “ 区分控制 ” 的问题,这里简单说下会出现的两个情况:

1、前台展示项目:如果我们的vue 项目,是一个前台网站,比如 电商类 的或者 Blog.Vue 这样的,很简单,我们只需要在 api 上加上 [Authorize] 这个无具体规则的授权特性就行,大家先不要往下看,先停一分钟想一想是不是这个情况。商城嘛,只需要用户登录一下就可以购买了,我们不需要特地的区分商城用户有什么区别,有什么三六九等,大家都是一样,登录了,就可以任何操作,无论是买东西,还是写文章,亦或者投票等等;

2、后台管理项目:但是!还有另一种情况,那就是后台管理,一个对用户身份要求特别严格的一个系统,我们肯定不能仅仅在 api 接口地址上,加上 [Authorize] 这个简单的特性就完事儿了,就比如我们的 Blog.Admin 项目,肯定需要一套复杂的授权策略机制,那就不得不用到用户的角色信息,或者其他的模块信息,这就是我上边说的 “区分控制”;(至于是基于角色的策略,还是模块化,我还在考虑中,目前先尝试角色管理)

3、猜想:你是不是想说使用基于角色+策略授权的** Hybrid Flow 混合模式**?别着急,以后的问题会说到,这里提出这个问题,就是向给大家一个思路的过程。

如果是第二种情况的话,我们在用户注册的时候,就需要带上 “角色” 这个信息,比如我这里先默认是一个 test 系统测试管理员的角色(这个暂时这么处理,后期我会再深入研究下,是不是这个模式,或者如果正再看的你很懂的话,欢迎指导下,不胜感激!),当然,如果你的项目不需要对用户的权限进行划分,就比如我上边的第一种情况,电商类,博客类,只要不是后台管理这种的前台系统,都很简单,只需要在 api 上加上 [Authorize] ,然后授权中心是不需要角色这个概念的。

我们学术讨论嘛,当然是从复杂的着手,就把角色给考虑进去了,现在先写死一个角色,我们以后的文章中会进一步讨论这个复杂的情况:

 [HttpPost]
[Route("account/register")]
[ValidateAntiForgeryToken] public async Task Register(RegisterViewModel model, string returnUrl = null, string rName = "AdminTest")
{
ViewData["ReturnUrl"] = returnUrl;
IdentityResult result = new IdentityResult(); // 模型校验
if (ModelState.IsValid)
{ // 判断用户名是否存在,说明:如果是DDD设计思想,这中查重应该是写在领域模型的。
var userItem = _userManager.FindByNameAsync(model.LoginName).Result; if (userItem == null)
{ // 转成我们的实体模型,说明:这种多个实体转换,可以使用 Dto
var user = new ApplicationUser
{
Email = model.Email,
UserName = model.LoginName,
LoginName = model.RealName,
sex = model.Sex,
age = model.Birth.Year - DateTime.Now.Year,
birth = model.Birth,
addr = "",
tdIsDelete = false }; // 创建用户,注意密码的规范,比如必须有大小写字母+数字+符号
result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded)
{ // 用户添加成功后,就需要添加声明了,看自己需要多少吧,可以自定义扩展
result = await _userManager.AddClaimsAsync(user, new Claim[]{ // 这个 Name ,就是 Jwt 的唯一名字,也是页面里展示的名称,比如是“测试账号”,而不是登录名的“test1”
new Claim(JwtClaimTypes.Name, model.RealName), new Claim(JwtClaimTypes.Email, model.Email), // 是否需要进行 Email 邮件验证
new Claim(JwtClaimTypes.EmailVerified, "false", ClaimValueTypes.Boolean), // 这里就是角色声明
new Claim(JwtClaimTypes.Role, rName)
}); if (result.Succeeded)
{ // 添加成功,可以直接登录,这个就比如是我们的博客项目或者电商项目,我们在授权中心注册成功后,直接登录了,跳转到前台了。 //await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
}
} else {
ModelState.AddModelError(string.Empty, $"{userItem?.UserName} already exists");
} // 收集全部异常数据,返回前台
AddErrors(result);
} return View(model);
}

上边的就是注册的主要代码,大家可以自己任意的扩展,然后重要的部分,我已经标红,也写上了详细的注释,特别简单,都能看懂。

这一 Part 都很平常,最重要的一个问题还是那个角色这一块,希望读到这里的都能看懂,想一想到底你的项目里需不需要这样的 Claim,不懂的欢迎来讨论。

3、更新 与 逻辑删除(有权限)

上边咱们说到了展示和添加,那下边就是说到更新了(这个操作我带上了最高的权限,必须是超级管理员才能操作 [Authorize(Roles = "SuperAdmin")] ),你会问,为啥要把删除和更新放到一起呢?其实我个人感觉逻辑是一样的,平时开发肯定也都知道,逻辑删除其实就是把“是否删除” 这个字段设置成 True 就行了,但是真的是这样么,我们慢慢往下看。

首先更新用户这个很简单的,我就不多说什么了,具体的可以看看代码,主要的逻辑就是平时的三步走:

1、查询出当前人Model;

2、用视图模型修改Model;

3、执行更新操作 _userManager.UpdateAsync(userItem); // 这里要说下就是,Identity 自带了很多扩展方法,大家需要自己好好的研究下,从而达到自己的相应目的。

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第4张图片
image

更新说完了,下边说说删除,删除其实本身就有两种情况:

1、逻辑删除,很自然,就是将数据更新下状态,比如我们可以用上边的方法,把当前操作人的 IsDeleted=True 即可,很简单;

2、物理删除,这个还是需要好好研究研究,我在官方的代码里,没有找到如何物理删除的方法,可能还是需要开发者自己定义扩展吧;

这就是我说的第二个 “Flag”? ,需要好好的思考思考,如果你已经忘了第一个 Flag 的话,请向上看,用户注册章节里的角色问题。

image

(更新 & 删除 有权限 动图)

4、重置密码

这个是目前为止稍微复杂一点的,需用用到流程,首先看动图吧:

image

(重置/更新密码 动图)

这个过程其实很简单,也是项目中必须使用到的功能,我相信任何一个网站,必须要用到这个重置和找回密码的功能吧,当然生产环境很复杂,可能需要邮箱或者手机等来处理动态链接,我这里只是提供一个思路,总结来说,流程说明如下:

1、输入当时注册邮箱;

2、获取包含动态 Code 的安全链接(可通过发邮件的形式);

3、根据安全链接,设置新密码;

4、重新登录;

核心代码(节选):

// 1、判断邮箱
var user = await _userManager.FindByEmailAsync(model.Email); // 2、生成重置密码回调链接
var code = await _userManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); var ResetPassword = $"Please reset your password by clicking here: link"; // 3、重置密码
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);

5、其他情况处理?

通过上边的简单说明,AccountController 这个控制器的内容, 咱们说完了,是不是就没有问题了呢,不是!我们要研究,就要研究透彻,大家肯定注意到了这个项目中,基本都说到了,但是在核心的快速启动文件夹 Quickstart 中,还有几个控制器没有说到:

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第5张图片
image

不光如此,在平时的开发中,我们还会遇到下边这几个业务逻辑操作:

1、如何找回注册邮箱? 2、如何通过发送邮件,从而达到邮件确认的目的? 3、如何实现FaceBook、Google登录? 4、如何更新用户的角色等Claims?

5、如何刷新 Token ?

上边红框中的那几个控制器都是什么意思?

下边四条业务逻辑又该如何实现?

当前项目是不是还有其他不为我们知道的秘密?以后的章节再慢慢展开,请关注。

不过我们既然已经完成用户的基本操作,我们就先停下上边的疑惑问题,往下走走,看看 IdentityServer4 到底是如何通过 OpenID Connect 来操作的。

二、简单授权模式 —— Implicit Flow OpenID

0、OpenID Connect授权模式

OPID 认证流程主要是由 OAuth2 的五种授权流程延伸而来的,它有以下 3 种:

  • Authorization Code Flow(授权码模式):基于OAuth2的授权码来换取Id Token和Access Token。
  • Implicit Flow(简化模式):基于OAuth2的Implicit流程获取Id Token和Access Token。
  • Hybrid Flow:混合Authorization Code Flow+Implici Flow获取Id Token和Access Token。

注:OpenID Connect 为什么没有基于OAuth2的Resource Owner Password Credentials Grant和Client Credentials Grant扩展,Resource Owner Password Credentials Grant是需要应用提供账号密码的,账号密码都有了在获取Id Token意义不大。Client Credentials Grant没有用户的参与所以获取Id Token 也没意义。这也能反映授权和认证的差异,以及只使用OAuth2来做身份认证的事情是远远不够的,也是不合适的。

1、概念

简化模式用于获取访问令牌(但它不支持令牌的刷新,之所以所以称为简化模式,和授权码模式比少了获取授权码的步骤),并对运行特定重定向URI的公共客户端进行优化,而这一些列操作通常会使用脚本语言在浏览器中完成,令牌对访问者是可见的,且客户端也不需要验证。

简化模式,主要有下边三个特点:

1、用于“公共”客户端;

2、客户端应用直接从浏览器访问资源;

3、没有显式的客户端身份认证;

2、结构图

为了配合大家理解,我这里有两个场景,大家脑子里先有个画面,然后往下看四个角色和流程图:

场景一:博客园登录,需要获取腾讯的某一个QQ用户的头像和昵称等资源;

场景二:前后端分离,Vue 项目需要获取 Core 项目的 当前test1账号的 数据;

首先先理解下四个角色:

1、Resource Owner(资源拥有者) —— 资源所有者,就比如我们授权登录中的,QQ用户,他才是资源的拥有者。3143422472 /** test1账号**

2、Resource Server(资源服务器) —— 资源服务器,用来存储用户资源(头像,昵称等)的服务器,比如腾讯QQ。腾讯QQ服务器 / Blog.Core

3、Client(客户端) —— 第三方客户端,比如博客园;https://www.cnblogs.com /** Blog.Vue**

4、Authorization Server(授权服务器)—— 授权服务器,用来作为认证第三方平台的服务,比如腾讯的QQ互联平台。https://graph.qq.com/oauth2.0/show?whic...... / Blog.Idp

然后咱们看看具体的流程是怎样的:

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第6张图片
image

(流程1:参考网上画的,可能不是很明了)

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第7张图片
image

(流程2:自己根据官网图片做了下修改)

Tips:Web-Hosted Client Resource 服务器相当于是一个存储 accessToken 的地方,通常指浏览器中的存储(cookie、localStorage、SessionStorge、js变量等),一般这个页面是看不到的,而且一般情况是和 Client 客户端写在一起的,当然也有分开的。

步骤解析:

  1. 客户端携带客户端标识以及重定向URI到授权服务器;

  2. 用户确认是否要授权给客户端;

  3. 授权服务器得到许可后,跳转到指定的重定向地址,并将令牌也包含在了里面;

  4. 客户端不携带上次获取到的包含令牌的片段,去请求资源服务器;

  5. 资源服务器会向浏览器返回一个脚本;

  6. 浏览器会根据上一步返回的脚本,去提取在C步骤中获取到的令牌;

  7. 浏览器将令牌推送给客户端。

(A步骤)中需要用到的参数,注意在这里要使用"application/x-www-form-urlencoded"格式:

  • response_type 必选项,此值必须为"token"

  • client_id 必选项

  • redirect_uri 可选项

  • scope 可选项

  • state 建议选项

    例如:

GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
(C步骤)中返回的参数包含:
  • access_token 必选项

  • token_type 必选项

  • expires_in 建议选项

  • scope 可选项

  • state 必选项

    例如:

HTTP/1.1 302 Found
     Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600

上边我们简单的说了说 Implicit Flow 模式的相关知识点,不知道大家有没有一点点感觉,如果不是很懂,正好感觉配合着下边的代码研究下,二者结合会更好。

三、前后端项目授权联调

因为我们用到了前后端分离项目,所以一定是要三方处理,如果你现在使用的是 MVC 模式的话,我们以后的章节也会说到 授权码授权模式(Authorization Code Flow),这里先把简化模式调通了:

1、授权服务端 —— Implicit(Blog.Idp)

这个配置很简单,在 Blog.Idp 项目中,大家别看是在 Config.cs 文件里,其实它已经在我们上一篇文章中,生成到了数据库中,不懂的请回看上一篇文章

 new Client {
ClientId = "blogvuejs",//客户端id
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" }
}

13652172-c49e7b7f01ad28f1.png
image

2、资源服务端 —— Bearer(Blog.Core)

这里的配置是在 Blog.Core 我们的资源服务器中,在启动文件 Startup.cs 中,大家自行查看,注意如果使用这个的话,请把 Jwt 认证给注释掉:

services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options => {
options.Authority = "http://localhost:5002";//授权服务器地址
options.RequireHttpsMetadata = false;//是否Https
options.ApiName = "blog.core.api";//我们在 Blog.Idp 中配置的资源服务器名
});

从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调_第8张图片
image

添加过程中,可能会需要引用扩展包 : IdentityServer4.AccessTokenValidation 这都是小问题,大家自行检查即可。

3、请求客户端 —— Oidc(Blog.Vue)

上边我们已经在两个服务端做好了配置,客户端如何处理,这个地方才是今天的重头戏,无论是什么客户端,JS 或者 Vue、React、Ng 等等前端框架,都需要用到 oidc-client 这个插件库:

1、安装

执行命令:npm install oidc-client --save

2、封装

注意这个是一个js库,我们就像之前将 SignalR 那样,直接使用就行,不用在 main.js 中引用,但是还是需要先实例化一个用户管理类 ApplicationUserManager 并配置构造函数,请注意这些参数都要和 Blog.Idp 授权服务器配置一致。

在 src 文件夹下 新建 Auth 文件夹,并添加 applicationusermanager.js 来封装我们的连接管理:

import { UserManager } from 'oidc-client'

class ApplicationUserManager extends UserManager {
constructor () {
super({
authority: 'http://localhost:5002',// 授权服务中心地址
client_id: 'blogvuejs',// 客户端 id
redirect_uri: 'http://localhost:6688/callback',// 登录回调地址
response_type: 'id_token token',
scope: 'openid profile roles blog.core.api',// 作用域也要一一匹配
post_logout_redirect_uri: 'http://localhost:6688' //登出后回调地址 })
} async login () { await this.signinRedirect() return this.getUser()
} async logout () { return this.signoutRedirect()
}
}

同时为了配合其他页面使用,我们封装几个常用的方法,在 Auth 文件夹下,新建 UserAuth.js 来封装用户的一些基本信息:

import applicationUserManager from "./applicationusermanager"; const userAuth = {
data() { return {
user: {
name: "",
isAuthenticated: false }
};
},
methods: { async refreshUserInfo() {//获取用户信息 const user = await applicationUserManager.getUser(); if (user) { this.user.name = user.profile.name; this.user.isAuthenticated = true;
} else { this.user.name = ""; this.user.isAuthenticated = false;
}
}
}, async created() { await this.refreshUserInfo();
}
};
export default userAuth;

3、发起 登录/登出 请求

我们封装好了方法,下边就是直接设计业务逻辑了,过程很简单,在 App.vue 组件中:

1、每次路由跳转需异步获取用户数据;

2、发起异步登录请求;

3、发起异步登出请求;

import applicationUserManager from "./Auth/applicationusermanager";
import userAuth from "./Auth/UserAuth";

export default {
name: "app",
mixins: [userAuth],
data: function() { return {};
},
watch: {
root.store.commit("saveToken", "");
} catch (error) {
console.log(error); this.emit("show-snackbar", { message: error });
}
}
}
};

4、回调

在上边的用户管理配置中,我们用到了一个回调页面,这个很重要,因为我们在登录成功后,需要调整到客户端,并且需要将信息给存储下来,就是上边流程图中,我们说到的 客户端资源

13652172-dea4b00fbc573c73.png
image

具体怎么写的,很简单,在 views 视图页面文件夹下,新建一个 LoginCallbackView.vue 页面:

import applicationUserManager from '../Auth/applicationusermanager' export default { async created () { try { // 核心的就是这里了
await applicationUserManager.signinRedirectCallback()
let user = await applicationUserManager.getUser() // 将 token 存储在客户端
this.router.push({name: 'home'})
} catch (e) {
console.log(e) this.emit('show-snackbar', { message: e })
}
}
}

四、结语

本文还是延续上篇文章的快速讲解的风格,简单连贯的把用户管理和前后端联调的内容通了一遍,总结一下:

1、分析了用户是否需要角色等策略的缘由;

2、实现了对用户的基本操作——CURD+重置密码;

3、授权项目中还遗留了一片未知的知识块,亟待探索;

4、实现了客户端、资源服务器、授权服务器的第一次联调;

5、重点讲解了五大模式中的 Implicit Flow 简化模式的概念和应用场景;

6、同时也把 Hybrid Flow 混合模式给引申出来,因为它基于 角色+策略 的授权;

当然,通过这一篇的学习,又开拓出了更多的未知领域,IdentityServer4 没有我们想想的那么难,但是肯定也不是一个 Demo 就能说的完的简单,

如何解决文章中提到的,打算提到的,未提到的各种问题呢,请持续关注吧。

五、Github && Gitee

https://github.com/anjoy8/Blog.IdentityServer

你可能感兴趣的:(从壹开始 [ Ids4实战 ] 之四 ║ 用户数据管理 & 前后端授权联调)