一, 前言
上几篇文章主要分享了 IdentityServer4 在 ASP.NET http://xn--asp-lp6e.net/ Core 3.x 中的应用, 在上面的几篇分享中有一部分博友问了我这么一个问题 "他通过 IdentityServer4 来搭建授权中心网关服务, 怎么才能在访问受保护的 Api 资源中获取到用户的相关的身份信息呢?".
那这篇文章主要来分享认证过程中的一个重要组成部分 Claim, 在开始之前强烈建议还没看过我写的 IdentityServer4 系列文章的同学先看一下, 下面几篇文章中以架构思维带大家进入 IdentityServer4 的世界
ASP.NET Core IdentityServer4 中的基本概念
ASP.NET Core 中 IdentityServer4 授权中心之应用实战
ASP.NET Core 中 IdentityServer4 授权中心之自定义授权模式
ASP.NET Core 中 IdentityServer4 授权原理及刷新 Token 的应用
二, Claim 是什么
Claim
Claim 我的理解是一个声明, 存储着一个键值对的关系, 就相当于身份证中的 姓名: 特朗普 , 性别: 男等等身份证的系列元素, 每一个项都是一个键值, 我们看看 Claim 主要代码
- public class Claim
- {
- public string ClaimType { get; set; }
- public string ClaimValue { get; set; }
- }
代码中主要核心两个属性 ClaimType 和 ClaimValue;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
来源: https://www.cnblogs.com/jlion/p/12543486.html