背景
在设计系统时,我们必然要考虑系统使用的用户,不同的用户拥有不同的权限。主流的权限管理系统都是RBAC模型(Role-Based Access Control 基于角色的访问控制)的变形和运用,只是根据不同的业务和设计方案,呈现不同的显示效果。
在微软文档中我们了解了《基于角色的授权》,但是这种方式在代码设计之初,就设计好了系统角色有什么,每个角色都可以访问哪些资源。针对简单的或者说变动不大的系统来说这些完全是够用的,但是失去了灵活性。因为我们不能自由的创建新的角色,为其重新指定一个新的权限范围,毕竟就算为用户赋予多个角色,也会出现重叠或者多余的部分。
RBAC(Role-Based Access Control)即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
graph LR
A[用户] -->| 属于多个 | B(角色) -->| 角色拥有 | C(权限)
RBAC模型可以分为:RBAC0、RBAC1、RBAC2、RBAC3 四种。其中RBAC0是基础,也是最简单的,今天我们就先从基础的开始。
资源描述的管理
在开始权限验证设计之前我们需要先对系统可访问的资源进行标识和管理。在后面的权限分配时,我们通过标识好的资源进行资源和操作权限的分配。
资源描述
创建一个 ResourceAttribute
继承 AuthorizeAttribute
和 IAuthorizationRequirement
资源描述属性,描述访问的角色需要的资源要求。通过转化为 Policy
来对 策略的授权 提出要求。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ResourceAttribute: AuthorizeAttribute, IAuthorizationRequirement
{
private string _resouceName;
private string? _action;
/// <summary>
/// 设置资源类型
/// </summary>
/// <param name="name">资源名称</param>
/// <exception cref="ArgumentNullException">资源名称不能为空</exception>
public ResourceAttribute(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
string[] resourceValues = name.Split('-');
_resouceName = resourceValues[0];
if (resourceValues.Length > 1)
{
Action = resourceValues[1];
}
else
{
Policy = resourceValues[0];
}
}
/// <summary>
/// 获取资源名称
/// </summary>
/// <returns></returns>
public string GetResource()
{
return _resouceName;
}
/// <summary>
/// 获取操作名称
/// </summary>
public string? Action
{
get
{
return _action;
}
set
{
_action = value;
if (!string.IsNullOrEmpty(value))
{
//把资源名称跟操作名称组装成Policy
Policy = _resouceName + "-" + value;
}
}
}
}
获得所有资源
我们标识好系统中的资源后,还需要获取到我们最终程序中都标识有哪些资源,这里就需使用 ASP.NET Core 中的应用程序模型。可以在程序启动时获取到所有的 Controller 和 Controller 中的每一个方法,然后通过查询 ResourceAttribute 将其统一存储到静态类中。
创建一个 ResourceInfoModelProvider
继承 IApplicationModelProvider
,其执行顺序我们设置为=> -989
。其执行顺序:
- 首先 (Order=-1000):
DefaultApplicationModelProvider
- 然后(Order= -990):
AuthorizationApplicationModelProvider
CorsApplicationModelProvider
- 接着是这个
ResourceInfoModelProvider
其核心代码如下:
/// <summary>
/// 基于其 Order 属性以倒序调用
/// </summary>
/// <param name="context"></param>
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
//获取所有的控制器
List<ResourceAttribute> attributeData = new List<ResourceAttribute>();
foreach (var controllerModel in context.Result.Controllers)
{
//得到ResourceAttribute
//Controller 的特性
var resourceData = controllerModel.Attributes.OfType<ResourceAttribute>().ToArray();
if (resourceData.Length > 0)
{
attributeData.AddRange(resourceData);
}
//Controller 中的每个方法的特性
foreach (var actionModel in controllerModel.Actions)
{
var actionResourceData = actionModel.Attributes.OfType<ResourceAttribute>().ToArray();
if (actionResourceData.Length > 0)
{
attributeData.AddRange(actionResourceData);
}
}
}
// 整理信息集中存入全局
foreach (var item in attributeData)
{
ResourceData.AddResource(item.GetResource(), item.Action);
}
}
授权控制的实现
接下来我们要对授权控制来进行编码实现,包含自定义授权策略的实现和自定义授权处理程序。
动态添加自定义授权策略
关于自定义授权策略提供程序的说明,这里不再赘述微软的文档,里面已经介绍了很详细,这里我们通过其特性可以动态的创建自定义授权策略,在访问资源时我们获取到刚刚标识的 Policy
没有处理策略,就直接新建一个,并传递这个策略的权限检查信息,当然这只是一方面,更多妙用,阅读文档里面其适用范围的说明即可。
/// <summary>
/// 自定义授权策略
/// 自动增加 Policy 授权策略
/// </summary>
/// <param name="policyName">授权名称</param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// 检查这个授权策略有没有
AuthorizationPolicy? policy = _options.GetPolicy(policyName);
if (policy is null)
{
_options.AddPolicy(policyName, builder =>
{
builder.AddRequirements(new ResourceAttribute(policyName));
});
}
return Task.FromResult(_options.GetPolicy(policyName));
}
授权处理程序
前面我们已经可以动态创建授权的策略,那么关于授权策略的处理我们可以实现 AuthorizationHandler
根据传递的策略处理要求对本次请求进行权限的分析。
internal class ResourceAuthorizationHandler : AuthorizationHandler<ResourceAttribute>
{
/// <summary>
/// 授权处理
/// </summary>
/// <param name="context">请求上下文</param>
/// <param name="requirement">资源验证要求</param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAttribute requirement)
{
// 需要有用户
if (context.User is null) return Task.CompletedTask;
if (context.User.IsInRole(ResourceRole.Administrator) // 超级管理员权限,拥有 SangRBAC_Administrator 角色不检查权限
|| CheckClaims(context.User.Claims, requirement) // 符合 Resource 或 Resource-Action 组合的 Permission
)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
/// <summary>
/// 检查 Claims 是否符合要求
/// </summary>
/// <param name="claims">待检查的claims</param>
/// <param name="requirement">检查的依据</param>
/// <returns></returns>
private bool CheckClaims(IEnumerable<Claim> claims, ResourceAttribute requirement)
{
return claims.Any(c =>
string.Equals(c.Type, ResourceClaimTypes.Permission, StringComparison.OrdinalIgnoreCase)
&& (string.Equals(c.Value, requirement.GetResource(), StringComparison.Ordinal)
|| string.Equals(c.Value, $"{requirement.GetResource()}-{requirement.Action}", StringComparison.Ordinal))
);
}
}
这里我们提供了一个内置固定角色名的超级管理员用户,其请求不进行权限检查。
最后
这里我们已经实现了简单的 RBAC 权限设计,之后我们主要在生成 JWT 时带上可访问资源的Permission
即可。
new Claim(ResourceClaimTypes.Permission,"查询")
当然,如果直接放在 jwt 中会让 Token 变得很长,虽然我其实并不理解微软的 ClaimTypes 使用一个URI标识,如果有了解的朋友可以帮我解个惑,万分感谢 https://stackoverflow.com/questions/72293184/ 。
回到这个问题,我们可以再设计一个中间件,在获取到用户的角色名时将其关于角色权限的ClaimTypes
加入到 content.User
即可。关于这一方面的详细介绍和实现可以看下一篇文章。
本文介绍的相关代码已经提供 Nuget 包,并开源了代码,感兴趣的同学可以查阅:
https://github.com/sangyuxiaowu/Sang.AspNetCore.RoleBasedAuthorization
如有错漏之处,敬请指正。