Asp.Net Core 中IdentityServer4 实战之角色授权详解
一、前言
前几篇文章分享了 IdentityServer4 密码模式的基本授权及自定义授权等方式,最近由于改造一个网关服务,也用到了 IdentityServer4 的授权,改造过程中发现比较适合基于 Role 角色的授权,通过不同的角色来限制用户访问不同的 Api资源 ,这里我就来分享 IdentityServer4 基于角色的授权详解。
IdentityServer4 历史文章目录
没有看过之前的几篇文章,我建议先回过头看看上面那几篇文章再来看本篇文章,不过对于大牛来说就可以跳过了。。。。
二、模拟场景
还是按照我的文章风格套路,实战之前先来模拟下应用场景,无场景的实战都是耍流氓,模拟场景更能让大家投入,同时也是自我学习、思考、总结的结晶之处!!!
对于角色授权大家也不陌生,大家比较熟悉的应该是 RBAC 的设计,这里就不阐述 RBAC ,有兴趣的可以百度。我们这里简单模拟下角色场景假如有这么一个 数据网关服务 服务(下面我统称为 数据网关 ),客户端有三种账号角色(普通用户、管理员用户、超级管理员用户),数据网关针对这三种角色用户分配不同的数据访问权限,场景图如下:
那么这种场景我们会怎么去设计呢?这个场景还算比较简单,角色比较单一,比较固定,对于这种场景很多人可能会考虑到通过 Filter 过滤器等方式来实现,这当然可以。不过针对这种场景 IdentityServer4 中本身就支持角色授权,下面我来给大家分享 IdentityServer4 的角色授权.
三、角色授权实战授权流程
撸代码之前我们先整理下 IdentityServer4 的 角色授权流程图,我简单概括画了下,流程图如下:
场景图概括如下:
-
客户端分为三种核心角色(普通用户、管理员用户、超级管理-老板)用户,三种用户访问同一个 数据网关 (API资源)
-
数据网关 (API资源)对这三种用户角色做了访问限制。
角色授权流程解释如下:
-
第一步:不同的用户携带用户密码等信息访问 授权中心 (ids4)尝试授权
-
第二步: 授权中心 对用户授权通过返回 access_token 给用户同时声明用户的 Role 到 Claim 中。。
-
第三步:客户端携带拿到的 access_token 尝试请求 数据网关 (API资源)。
-
第四步: 数据网关 收到客户端的第一次请求会到 授权中心 请求获得验证公钥。
-
第五步: 授权中心 返回 验证公钥 给 数据网关 并且缓存起来,后面不再到 授权中心 再次获得验证公钥(只会请求一次,除非重启服务)。
-
第六步: 数据网关 (ids4)通过验证网关验证 access_token 是否验证通过,并且验证请求的客户端用户声明的 Role 是否和请求的 API资源 约定的的角色一致。如果一致则通过第步返回给用户端,否则直接拒绝请求.
撸代码
代码继续上面几篇文章的例子的续集,你懂的,就不从零开始撸代码啦(强烈建议没看过上面几篇的先看下上面的目录中的几篇,要不然会一头雾水,大佬跳过)要使 IdentityServer4 实现的 授权中心 支持角色验证的支持,我们需要在定义的 API资源 中添加 角色 的引入,代码如下:上几篇文章的 授权中心 (Jlion.NetCore.Identity.Service)的代码如下:
/// <summary>
/// 资源
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources
{
return new List<ApiResource>
{
new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
};
}
加入角色的支持代码改造如下:
/// <summary>
/// 资源
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApiResources
{
return new List<ApiResource>
{
new ApiResource(
OAuthConfig.UserApi.ApiName,
OAuthConfig.UserApi.ApiName,
new List<string>{JwtClaimTypes.Role }
),
};
}
API资源 中添加了 角色 验证的支持后,需要在用户登录授权成功后声明Claim用户的 Role 信息,代码如下:改造前代码:
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("登录失败,用户名和密码不正确");
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
}
为了保留之前文章的源代码,好让之前的文章源代码可追溯,我这里不在源代码上改造升级,我直接新增一个用户密码验证器类,命名为 RoleTestResourceOwnerPasswordValidator ,代码改造如下:
/// <summary>
/// 角色授权用户名密码验证器demo
/// </summary>
public class RoleTestResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var userName = context.UserName;
var password = context.Password;
//验证用户,这么可以到数据库里面验证用户名和密码是否正确
var claimList = await ValidateUserByRoleAsync(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>
/// 验证用户(角色Demo 专用方法)
/// 这里和之前区分,主要是为了保留和博客同步源代码
/// </summary>
/// <param name="loginName"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<List<Claim>> ValidateUserByRoleAsync(string loginName, string password)
{
//TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
// 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
var user = OAuthMemoryData.GetUserByUserName(loginName);
if (user == null)
throw new Exception("登录失败,用户名和密码不正确");
//下面的Claim 声明我为了演示,硬编码了,
//实际生产环境需要通过读取数据库的信息并且来声明
return new List<Claim>
{
new Claim(ClaimTypes.Name, $"{user.UserName}"),
new Claim(EnumUserClaim.DisplayName.ToString,user.DisplayName),
new Claim(EnumUserClaim.UserId.ToString,user.UserId.ToString),
new Claim(EnumUserClaim.MerchantId.ToString,user.MerchantId.ToString),
new Claim(JwtClaimTypes.Role.ToString,user.Role.ToString)
};
}
#endregion
}
为了方便演示,我直接把 Role 定义成了一个公共枚举 EnumUserRole ,代码如下:
/// <summary>
/// 角色枚举
/// </summary>
public enum EnumUserRole
{
Normal,
Manage,
SupperManage
}
GetUserByUserName 中硬编码创建了三个角色的用户,代码如下:
/// <summary>
/// 为了演示,硬编码了,
/// 这个方法可以通过DDD设计到底层数据库去查询数据库
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public static UserModel GetUserByUserName(string userName)
{
var normalUser = new UserModel
{
DisplayName = "张三",
MerchantId = 10001,
Password = "123456",
Role = Enums.EnumUserRole.Normal,
SubjectId = "1",
UserId = 20001,
UserName = "testNormal"
};
var manageUser = new UserModel
{
DisplayName = "李四",
MerchantId = 10001,
Password = "123456",
Role = Enums.EnumUserRole.Manage,
SubjectId = "1",
UserId = 20001,
UserName = "testManage"
};
var supperManageUser = new UserModel
{
DisplayName = "dotNET博士",
MerchantId = 10001,
Password = "123456",
Role = Enums.EnumUserRole.SupperManage,
SubjectId = "1",
UserId = 20001,
UserName = "testSupperManage"
};
var list = new List<UserModel> {
normalUser,
manageUser,
supperManageUser
};
return list?.Where(item => item.UserName.Equals(userName))?.FirstOrDefault;
}
好了,现在用户授权通过后声明的 Role 也已经完成了,我上面使用的是JwtClaimTypes 默认支持的 Role ,你也可以不使用 JwtClaimTypes 类,可以自定义类来实现。最后为了让新关注我的博客用户没看过之前几篇文章的用户不至于一头雾水,我把注册 ids 中间件代码还是贴出来,注册新的用户名密码验证器到DI中 代码如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers;
#region 数据库存储方式
services.AddIdentityServer
.AddDeveloperSigningCredential
.AddInMemoryApiResources(OAuthMemoryData.GetApiResources)
//.AddInMemoryClients(OAuthMemoryData.GetClients)
.AddClientStore<ClientStore>
//.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>
.AddResourceOwnerValidator<RoleTestResourceOwnerPasswordValidator>
.AddExtensionGrantValidator<WeiXinOpenGrantValidator>
.AddProfileService<UserProfileService>;//添加微信端自定义方式的验证
#endregion
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment)
{
app.UseDeveloperExceptionPage;
}
//使用IdentityServer4 的中间件
app.UseIdentityServer;
app.UseRouting;
app.UseAuthorization;
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers;
});
}
授权中心 的角色支持代码撸完了,我们来改造上几篇文章中说到的 用户网关 服务,这里我就叫 数据网关 ,项目: Jlion.NetCore.Identity.UserApiService 上一篇关于Asp.Net Core 中IdentityServer4 实战之 Claim详解文章中在 数据网关 服务中新增了 UserController 控制器,并添加了一个访问用户基本的 Claim 信息接口,之前的代码如下:
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly ILogger<UserController> _logger;
public UserController(ILogger<UserController> logger)
{
_logger = logger;
}
[Authorize]
[HttpGet]
public async Task<object> Get
{
var userId = User.UserId;
return new
{
name = User.Name,
userId = userId,
displayName = User.DisplayName,
merchantId = User.MerchantId,
};
}
}
上面的代码中 Authorize 没有指定 Role ,那相当于所有的用户都可以访问这个接口,接下来,我们在 UserController 中创建一个只能是 超级管理员 角色才能访问的接口,代码如下
[Authorize(Roles =nameof(EnumUserRole.SupperManage))]
[HttpGet("{id}")]
public async Task<object> Get(int id)
{
var userId = User.UserId;
return new
{
name = User.Name,
userId = userId,
displayName = User.DisplayName,
merchantId = User.MerchantId,
roleName=User.Role//获得当前登录用户的角色
};
}
到这里 数据网关 代码也已经改造完了,我们接下来就是运行结果看看是否正确。
运行
我们分别通过命令行运行我们的 授权网关 服务和 数据网关 服务,分别如下图: 授权网关 还是指定5000 端口,如下图:
数据网关 跟之前几篇文章一样指定 5001 端口,如下图:
现在 授权网关 和 数据网关 都已经完美运行起来了,接下来我们通过 postman 模拟请求。先来通过普通用户(testNormal)请求 授权中心 获得 access_token ,如下图:
请求验证通过,再来通过获取到的 access_token 获取普通接口:
也完美获取到数据再来访问下标注了 supperManage 超级管理员的角色接口,如下图:
结果跟预想的一样,返回了 403 访问被拒绝,其他账号运行也是一样,我这里就不一一去运行访问测试了,有兴趣的同学可以到github 上拉起我的源代码进行运行测试,到这里基于 ids4 角色授权基础应用也完成了。
结束语: 上面分享学习了IdentityServer4 进行角色授权的实战例子,但是从上面的例子中可以发现Controller 或者Action 中指定Role 的使用场景不是很广泛,对于固定的那种角色场景比较适用,但是对于一个庞大的系统来说,用户的权限、角色和API资源是后台灵活可以分配的,这种场景感觉就不是很合适,那IdentityServer4 有没有什么好的方式实现呢?留给大家思考,思考就是思维的一大进步。
感谢语:三月份即将过去,三月份同时也是美好的开始,我的博客从三月份开始整理分享,传承着以一起学习,共同进步为目标,自我自律,开始分享相关技术。文章持续性同步至我的微信公众号【dotNET博士】,这个月来初见成效,一个月内已经荣获500+以上的粉丝,也感谢大家一直以来对我的关注,你的关注让我更有动力分享更好的原创技术文章。还没有关注微信公众号的,搜索"dotNET博士"关注,或者微信扫下面的二维码进行关注,同时大家也可以积极的分享或点个右下角的 推荐,让更多人的关注到我的文章。