一、前言

由于疫情原因,让我开始了以博客的方式来学习和分享技术(持续分享的过程也是自己学习成长的过程),同时也让更多的初学者学习到相关知识,如果我的文章中有分析不到位的地方,还请大家多多指教;以后我会持续更新我的文章,望大家多多支持和关注。

上几篇文章主要分享了IdentityServer4在Asp.Net Core 3.x 中的应用,在上面的几篇分享中有一部分博友问了我这么一个问题"他通过IdentityServer4 来搭建授权中心网关服务,怎么才能在访问受保护的Api资源中获取到用户的相关的身份信息呢?"。

那这篇文章主要来分享认证过程中的一个重要组成部分Claim,在开始之前强烈建议还没看过我写的 IdentityServer4 系列文章的同学先看一下,下面几篇文章中以架构思维带大家进入IdentityServer4 的世界

二、Claim 是什么

Claim

Claim 我的理解是一个声明,存储着一个键值对的关系,就相当于身份证中的 姓名:特朗普 , 性别:男等等身份证的系列元素,每一个项都是一个键值,我们看看Claim主要代码

public class Claim
{
public string ClaimType { get; set; } public string ClaimValue { get; set; }
}

代码中主要核心两个属性 ClaimTypeClaimValue;ClaimType 就是Key,ClaimValue就代表一个Value。这样的话,刚好可以存储一个键值对。这时候姓名:特朗普是不是就可以存进去了。

同时微软也提供了默认的ClaimType,部分默认的如下图:



Claim 差不多已经介绍完毕,相对比较简单清晰,Claim可以说是身份的最小单元的声明(身份单元)。

ClaimsIdentity

我们先来看看ClaimsIdentity的部分代码,代码如下:

public class ClaimsIdentity:IIdentity
{
public ClaimsIdentity(IEnumerable<Claim> claims){} //名字这么重要,当然不能让别人随便改啊,所以我不许 set,除了我儿子跟我姓,所以是 virtual 的
public virtual string Name { get; }
public string Label { get; set; } //身份单元集合
public virtual IEnumerable<Claim> Claims { get; } //这是我的证件类型,也很重要,同样不许 set
public virtual string AuthenticationType { get; } public virtual void AddClaim(Claim claim); public virtual void RemoveClaim(Claim claim); public virtual void FindClaim(Claim claim);
}

从代码中可以看到有一个Claims 属性,是一个集合,看到这里是不是可以把我们的身份证给联想进去呢?我们每个人都有一个“身份证”(ClaimIdentity),身份证中包含了多个“身份单元”(Claim)等信息。

从代码中还有一个特别重要的属性AuthenticationType 翻译成认证类型,这里也就相当于证件类型,比如身份证,它的证件类型就是"身份证",护照证机的证机类型就是"护照"。

同时ClaimsIdentity继承了IIdentity抽象接口,我们再来看看这个抽象接口的代码:

// 定义证件对象的基本功能。
public interface IIdentity
{
//证件名称
string Name { get; } // 用于标识证件的载体类型。
string AuthenticationType { get; } //是否是合法的证件。
bool IsAuthenticated { get; }
}

到这里ClaimsIdentity介绍的差不多了,ClaimsIdentity就相当于是身份证、或者护照之类的东西,一个能够证明身份的证件。

ClaimsPrincipal

一个人有了身份,就会有多重身份,比如你即是司机校长公司老板等等,那你就会有驾驶证教师证公司的营业执照等证件。那这些证件需要一个载体去容纳,那ClaimsPrincipal这个就相当于是这些证件的载体,我们来看看它的部分核心代码:

    public class ClaimsPrincipal : IPrincipal
{ public ClaimsPrincipal(); public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities); public ClaimsPrincipal(IIdentity identity); public ClaimsPrincipal(IPrincipal principal); public virtual IIdentity Identity { get; } public virtual IEnumerable<ClaimsIdentity> Identities { get; } //把证件添加到载体中
public virtual void AddIdentities(IEnumerable<ClaimsIdentity> identities);
//把证件添加到载体中
public virtual void AddIdentity(ClaimsIdentity identity); //以下都是从载体中获取证件等操作
public virtual IEnumerable<Claim> FindAll(Predicate<Claim> match); public virtual IEnumerable<Claim> FindAll(string type); public virtual Claim FindFirst(string type); public virtual Claim FindFirst(Predicate<Claim> match); public virtual bool HasClaim(Predicate<Claim> match); //是否属于某个角色
public virtual bool IsInRole(string role);
}

ClaimsPrincipal 介绍完了,我这里把ClaimsPrincipal 它叫证件的容器载体

我们已经知道了 “身份单元(Claims)” , “身份证(ClaimsIdentity)” , “证件容器载体(ClaimsPrincipal)”这三者的关系。

我们简单的来看下身份认证携带信息的简化的流程图:



好了,这里Claim相关概念理清楚了,这里也需要感谢下 微软MVP大佬@Savorboard 的文章https://www.cnblogs.com/savorboard/p/aspnetcore-identity.html 让我理清楚了这些关系!

三、实战

我这里继续我上几篇文章的代码基础上编写,需要代码的可以访问 https://github.com/a312586670/IdentityServerDemo 代码会跟着博客同步更新。

上几篇文章中解决方案中已经创建了如下三个项目:

  • Jlion.NetCore.Identity :Identity公共基础类库
  • Jlion.NetCore.Identity.Service : Ids4授权服务,也是上几篇文章中说的授权中心服务简单版本
  • Jlion.NetCore.Identity.UserApiService :用户业务网关(受保护的资源)

授权中心(Ids4授权服务)

Jlion.NetCore.Identity.Service

我们先在授权中心(ids4)服务中验证用户的代码中添加用户的相关Claims,核心代码如下:

不熟悉的请先移步

Asp.Net Core 中IdentityServer4 授权中心之应用实战 这篇文章

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var userName = context.UserName;
var password = context.Password; //验证用户,这么可以到数据库里面验证用户名和密码是否正确
var claimList = await ValidateUserAsync(userName, password); // 验证账号
context.Result = new GrantValidationResult
(
subject: userName,
authenticationMethod: "custom",
claims: claimList.ToArray()
);
}
catch (Exception ex)
{
//验证异常结果
context.Result = new GrantValidationResult()
{
IsError = true,
Error = ex.Message
};
}
} #region Private Method
/// <summary>
/// 验证用户
/// </summary>
/// <param name="loginName"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<List<Claim>> ValidateUserAsync(string loginName, string password)
{
//TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
// 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
var user = OAuthMemoryData.GetTestUsers(); if (user == null)
throw new Exception("登录失败,用户名和密码不正确");
//我这里为了测试,简单的硬编码,生产环境可以通过数据库中读取到相关信息构造`Claim`(**身份单元**)
return new List<Claim>()
{
new Claim(ClaimTypes.Name, $"{loginName}"),
new Claim(EnumUserClaim.DisplayName.ToString(),"测试用户"),
new Claim(EnumUserClaim.UserId.ToString(),"10001"),
new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"),
};
}
#endregion
}

现在,多个Claim已经构建完成,多个Claim构建出了一个用户身份,它们都属于即将登录的用户所拥有的身份单元,接下来我们还需要实现IProfileService抽象接口,

代码如下:

public class UserProfileService : IProfileService
{
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
try
{ var claims = context.Subject.Claims.ToList();
// 把认证通过的用户身份
context.IssuedClaims = claims.ToList();
}
catch { }
} /// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
}
}

GetProfileDataAsync主要为用户加载Claims,实现该代码并且通过AddProfileService<T>()方法添加到DI中,才能在API资源中获取到用户的身份信息,代码如下:

  public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(); #region 数据库存储方式
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
//.AddInMemoryClients(OAuthMemoryData.GetClients())
.AddClientStore<ClientStore>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddExtensionGrantValidator<WeiXinOpenGrantValidator>()//添加微信端自定义方式的验证(上篇自定义授权方式的代码)
.AddProfileService<UserProfileService>(); #endregion }

Api资源(受保护的资源)

Jlion.NetCore.Identity.UserApiService

我们先来创建UserIdentityExtension扩展类,代码如下:

 public static class UserIdentityExtension
{
/// <summary>
/// 获得用户的Name
/// </summary>
/// <param name="this"></param>
/// <returns></returns>
public static string Name(this ClaimsPrincipal @this)
{
return @this?.Identity?.Name;
} /// <summary>
/// 获得DisplayName
/// </summary>
/// <param name="this"></param>
/// <returns></returns>
public static string DisplayName(this ClaimsPrincipal @this)
{
var value = @this?.Claims?.FirstOrDefault(oo => oo.Type == EnumUserClaim.DisplayName.ToString())?.Value;
return value;
} public static string UserId(this ClaimsPrincipal @this)
{
return @this?.Claims?.FirstOrDefault(oo => oo.Type == EnumUserClaim.UserId.ToString())?.Value;
} public static string MerchantId(this ClaimsPrincipal @this)
{
return @this?.Claims?.FirstOrDefault(oo => oo.Type == EnumUserClaim.MerchantId.ToString())?.Value;
}
}

再在原来的代码基础上新增UserController控制器,代码如下:

[Authorize]
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{ private readonly ILogger<UserController> _logger; public UserController(ILogger<UserController> logger)
{
_logger = logger;
}
}

UserController控制器已经创建完了,继承了ControllerBase基类,我们来看看ControllerBase包含了哪些信息,核心的代码如下:

/// <summary>
/// A base class for an MVC controller without view support.
/// </summary>
[Controller]
public abstract class ControllerBase
{
//通过请求上下文中获得User(证件载体容器)
public ClaimsPrincipal User => HttpContext?.User; //其他核心代码没有贴出来,具体的可以看官方源代码
}

看了源代码,我们是不是可以考虑使用User来获取身份证件中的某些身份元件呢?,在UserController添加获取用户信息的接口,完整代码如下:

[Authorize]
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{ private readonly ILogger<UserController> _logger; public UserController(ILogger<UserController> logger)
{
_logger = logger;
} [HttpGet]
public async Task<object> Get()
{
//通过ClaimsPrincipal(证件容器载体)获得某些证件的身份元件
var userId = User.UserId();
return new
{
name = User.Name(),
userId = userId,
displayName = User.DisplayName(),
merchantId = User.MerchantId(),
};
}
}

好了,代码已经构建完成!!!

现在我把两个服务通过命令行启动起来.

Jlion.NetCore.Identity.Service启动如下图:



我这里还是以"http://localhost:5000" 地址启动

Jlion.NetCore.Identity.UserApiService 启动如下图:



这里以"http://localhost:5001"地址启动

现在我们通过postman 访问ids4服务器获得accesstoken 如下图:



获取access_token 成功,我再携带access_token访问 用户业务网关,如下图:

成功获取到用户身份信息 Claims相关信息。

结论:ids4授权服务中构建用户身份信息(Claim)通过身份容器载体ClaimsPrincipal载入(具体载入到哪里?是怎么携带到Api资源网关中的?下篇文章再来分享具体的原理和流程);再经过受保护的Api资源网关中通过ClaimsPrincipal身份容器载体获得当前用户的相关信息后就可以做一些基于角色授权业务相关的事情。

博客系列文章源代码地址:https://github.com/a312586670/IdentityServerDemo

参考文章:

https://www.cnblogs.com/savorboard/p/aspnetcore-identity.html

Asp.Net Core 中IdentityServer4 实战之 Claim详解的更多相关文章

  1. Asp.Net Core 中IdentityServer4 实战之角色授权详解

    一.前言 前几篇文章分享了IdentityServer4密码模式的基本授权及自定义授权等方式,最近由于改造一个网关服务,用到了IdentityServer4的授权,改造过程中发现比较适合基于Role角 ...

  2. Asp.Net Core 中IdentityServer4 授权中心之应用实战

    一.前言 查阅了大多数相关资料,查阅到的IdentityServer4 的相关文章大多是比较简单并且多是翻译官网的文档编写的,我这里在 Asp.Net Core 中IdentityServer4 的应 ...

  3. Asp.Net Core 中IdentityServer4 授权中心之自定义授权模式

    一.前言 上一篇我分享了一篇关于 Asp.Net Core 中IdentityServer4 授权中心之应用实战 的文章,其中有不少博友给我提了问题,其中有一个博友问我的一个场景,我给他解答的还不够完 ...

  4. Asp.Net Core 中IdentityServer4 授权原理及刷新Token的应用

    一.前言 上面分享了IdentityServer4 两篇系列文章,核心主题主要是密码授权模式及自定义授权模式,但是仅仅是分享了这两种模式的使用,这篇文章进一步来分享IdentityServer4的授权 ...

  5. ASP.NET Core MVC 源码学习:详解 Action 的激活

    前言 在 上一篇 文章中,我们已经学习了 ASP.NET Core MVC 的启动流程,那么 MVC 在启动了之后,当请求到达过来的时候,它是怎么样处理的呢? 又是怎么样把我们的请求准确的传达到我们的 ...

  6. ASP.NET Core MVC 源码学习:详解 Action 的匹配

    前言 在 上一篇 文章中,我们已经学习了 ASP.NET Core MVC 的启动流程,那么 MVC 在启动了之后,当请求到达过来的时候,它是怎么样处理的呢? 又是怎么样把我们的请求准确的传达到我们的 ...

  7. DevExpress ASP.NET Core Controls v18.2新功能详解

    行业领先的.NET界面控件2018年第二次重大更新——DevExpress v18.2日前正式发布,本站将以连载的形式为大家介绍新版本新功能.本文将介绍了DevExpress ASP.NET Core ...

  8. ASP.NET Core托管运行Quartz.NET作业调度详解

    Quartz.NET这么NB的作业调度系统,不会还行?   今天介绍一下Quartz.NET的托管运行,官网传送门. 一.前言 Quartz.NET,按官网上的说法,是一款功能齐全的任务调度系统,从小 ...

  9. IdentityServer4实战 - JWT Issuer 详解

    一.前言 本文为系列补坑之作,拖了许久决定先把坑填完. 下文演示所用代码采用的 IdentityServer4 版本为 2.3.0,由于时间推移可能以后的版本会有一些改动,请参考查看,文末附上Demo ...

随机推荐

  1. jquery.Table实现的翻页功能比较完整漂亮,本想扩展个模版DIV

    jquery.dataTable实现的翻页功能比较完整漂亮,本想提取其的翻页部分,再结合模版DIV,bootstrop实现聊天记息的展示. jquery.Table 与table结合的较紧,不能在很下 ...

  2. 传统if 从句子——以条件表达式作为if条件

    传统if 从句子——以条件表达式作为 if条件if [ 条件表达式 ]then command command commandelse command commandfi   条件表达式 文件表达式 ...

  3. Maximum Value(CodeForces - 484B)

    Maximum Value Time limit 1000 ms Memory limit 262144 kB You are given a sequence a consisting of n i ...

  4. 看完这篇还不了解 Nginx,那我就哭了!

    作者:蔷薇Nina www.cnblogs.com/wcwnina/p/8728391.html 想必大家一定听说过 Nginx,若没听说过它,那么一定听过它的"同行"Apache ...

  5. html|Area

    http://tomys.win/   HTML图片热区Area map的用法只是在上学的时候学习到过,在实际工作中一直没用过,如果 不是这次紧急任务,可能永远都不会想起这个功能.在一些特殊的html ...

  6. redis BLPOP

    一.需求 redis中保存了需要download的image url,存储格式为列表. 我需要从列表中获取数据,将图片下载保存到本地. 列表中的数据是一直增加的. 二.实现 使用redis BLPOP ...

  7. in和exists比较

    in是把外表和内表作hash 连接,而exists 是对外表作loop 循环,每次loop 循环再对内表进行查询. 一直以来认为exists 比in 效率高的说法是不准确的.如果查询的两个表大小相当, ...

  8. Solr查询配置及优化【eDisMax查询解析器】

    一.简介 Lucene查询解析器语法支持创建任意复杂的布尔查询,但还有一些缺点,它不是用户查询处理的理想解决方案.这里面最大的问题是Lucene查询解析器的语法要求严格,一旦破坏就会抛出异常.指望用户 ...

  9. Swift--struct与class的区别(汇编角度底层分析)

    概述 相对Objective-C, Swift使用结构体Struct的比例大大增加了,其中Int, Bool,以及String,Array等底层全部使用Struct来定义!在Swift中结构体不仅可以 ...

  10. 【WPF学习】第五十四章 关键帧动画

    到目前为止,看到的所有动画都使用线性插值从起点到终点.但如果需要创建具有多个分段的动画和不规则移动的动画.例如,可能希望创建一个动画,快速地将一个元素滑入到视图中,然后慢慢地将它移到正确位置.可通过创 ...