## 一、前言
前几篇文章分享了`IdentityServer4`密码模式的基本授权及自定义授权等方式,最近由于改造一个网关服务,用到了`IdentityServer4`的授权,改造过程中发现比较适合基于`Role`角色的授权,通过不同的角色来限制用户访问不同的`Api资源`,这里我就来分享`IdentityServer4`基于角色的授权详解。
#### IdentityServer4 历史文章目录
- [Asp.Net Core IdentityServer4 中的基本概念](https://www.cnblogs.com/jlion/p/12437441.html)
- [Asp.Net Core 中IdentityServer4 授权中心之应用实战](https://www.cnblogs.com/jlion/p/12447081.html)
- [Asp.Net Core 中IdentityServer4 授权中心之自定义授权模式](https://www.cnblogs.com/jlion/p/12468365.html)
- [Asp.Net Core 中IdentityServer4 授权原理及刷新Token的应用](https://www.cnblogs.com/jlion/p/12501195.html)
- [Asp.Net Core 中IdentityServer4 实战之 Claim详解](https:////www.cnblogs.com/jlion/p/12543486.html)
没有看过之前的几篇文章,我建议先回过头看看上面那几篇文章再来看本篇文章,不过对于大牛来说就可以跳过了。。。。
## 二、模拟场景
还是按照我的文章风格套路,实战之前先来模拟下应用场景,无场景的实战都是耍流氓,模拟场景更能让大家投入,同时也是自我学习、思考、总结的结晶之处!!!
对于角色授权大家也不陌生,大家比较熟悉的应该是`RBAC`的设计,这里就不阐述`RBAC`,有兴趣的可以百度。我们这里简单模拟下角色场景
假如有这么一个`数据网关服务`服务(下面我统称为`数据网关`),客户端有三种账号角色(普通用户、管理员用户、超级管理员用户),数据网关针对这三种角色用户分配不同的数据访问权限,场景图如下:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175216974-1045194802.jpg)
那么这种场景我们会怎么去设计呢?这个场景还算比较简单,角色比较单一,比较固定,对于这种场景很多人可能会考虑到通过`Filter`过滤器等方式来实现,这当然可以。不过正对这种场景`IdentityServer4`中本身就支持角色授权,下面我来给大家分享`IdentityServer4`的角色授权.
## 三、角色授权实战
#### 授权流程
撸代码之前我们先整理下`IdentityServer4`的 角色授权流程图,我简单概括画了下,流程图如下:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200328175347833-226144972.jpg)
场景图概括如下:
- 客户端分为三种核心角色(普通用户、管理员用户、超级管理-老板)用户,三种用户访问同一个`数据网关`(API资源)
- `数据网关`(API资源)对这三种用户角色做了访问限制。
角色授权流程解释如下:
- 第一步: 不同的用户携带用户密码等信息访问`授权中心`(ids4)尝试授权
- 第二步: `授权中心`对用户授权通过返回`access_token`给用户同时声明用户的`Role`到`Claim`中。。
- 第三步: 客户端携带拿到的`access_token`尝试请求`数据网关`(API资源)。
- 第四步:`数据网关`收到客户端的第一次请求会到`授权中心`请求获得验证公钥。
- 第五步:`授权中心`返回`验证公钥`给`数据网关`并且缓存起来,后面不再到`授权中心`再次获得验证公钥(只会请求一次,除非重启服务)。
- 第六步:`数据网关`(ids4)通过验证网关验证`access_token`是否验证通过,并且验证请求的客户端用户声明的`Role`是否和请求的`API资源`约定的的角色一致。如果一致则通过第步返回给用户端,否则直接拒绝请求.
#### 撸代码
代码继续上面几篇文章的例子的续集,你懂的,就不从零开始撸代码啦(强烈建议没看过上面几篇的先看下上面的目录中的几篇,要不然会一头雾水,大佬跳过)
要使`IdentityServer4`实现的`授权中心`支持角色验证的支持,我们需要在定义的`API资源`中添加`角色`的引入,代码如下:
上几篇文章的`授权中心`(Jlion.NetCore.Identity.Service)的
代码如下:
```
///
/// 资源
///
///
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
};
}
```
加入角色的支持代码改造如下:
```
///
/// 资源
///
///
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource(
OAuthConfig.UserApi.ApiName,
OAuthConfig.UserApi.ApiName,
new List
(){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
///
/// 验证用户
///
///
///
///
private async Task> ValidateUserAsync(string loginName, string password)
{
//TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
// 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
var user = OAuthMemoryData.GetTestUsers();
if (user == null)
throw new Exception("登录失败,用户名和密码不正确");
return new List()
{
new Claim(ClaimTypes.Name, $"{loginName}"),
new Claim(EnumUserClaim.DisplayName.ToString(),"测试用户"),
new Claim(EnumUserClaim.UserId.ToString(),"10001"),
new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"),
};
}
#endregion
}
```
为了保留之前文章的源代码,好让之前的文章源代码可追溯,我这里不在源代码上改造升级,我直接新增一个用户密码验证器类,
命名为`RoleTestResourceOwnerPasswordValidator`,代码改造如下:
```
///
/// 角色授权用户名密码验证器demo
///
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
///
/// 验证用户(角色Demo 专用方法)
/// 这里和之前区分,主要是为了保留和博客同步源代码
///
///
///
///
private async Task> ValidateUserByRoleAsync(string loginName, string password)
{
//TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
// 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
var user = OAuthMemoryData.GetUserByUserName(loginName);
if (user == null)
throw new Exception("登录失败,用户名和密码不正确");
//下面的Claim 声明我为了演示,硬编码了,
//实际生产环境需要通过读取数据库的信息并且来声明
return new List()
{
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`,代码如下:
```
///
/// 角色枚举
///
public enum EnumUserRole
{
Normal,
Manage,
SupperManage
}
```
`GetUserByUserName`中硬编码创建了三个角色的用户,代码如下:
```
///
/// 为了演示,硬编码了,
/// 这个方法可以通过DDD设计到底层数据库去查询数据库
///
///
///
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() {
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()
//.AddResourceOwnerValidator()
.AddResourceOwnerValidator()
.AddExtensionGrantValidator()
.AddProfileService();//添加微信端自定义方式的验证
#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详解](https://www.cnblogs.com/jlion/p/12543486.html)
文章中在`数据网关`服务中新增了`UserController`控制器,并添加了一个访问用户基本的`Claim`信息接口,之前的代码如下:
```
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly ILogger _logger;
public UserController(ILogger logger)
{
_logger = logger;
}
[Authorize]
[HttpGet]
public async Task