欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide

程序员文章站 2022-03-12 14:54:43
ASP.NET Core MVC 提供了基于角色( Role )、声明( Chaim ) 和策略 ( Policy ) 等的授权方式。在实际应用中,可能采用部门( Department , 本文采用用户组 Group )、职位 ( 可继续沿用 Role )、权限( Permission )的方式进行... ......

一、概述

asp.net core mvc 提供了基于角色( role )、声明( chaim ) 和策略 ( policy ) 等的授权方式。在实际应用中,可能采用部门( department , 本文采用用户组 group )、职位 ( 可继续沿用 role )、权限( permission )的方式进行授权。要达到这个目的,仅仅通过自定义 iauthorizationpolicyprovider 是不行的。本文通过自定义 iapplicationmodelprovide 进行扩展。

二、permissionauthorizeattribute : ipermissionauthorizedata

authorizeattribute 类实现了 iauthorizedata 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace microsoft.aspnetcore.authorization
{
/// <summary>
/// defines the set of data required to apply authorization rules to a resource.
/// </summary>
public interface iauthorizedata
{
/// <summary>
/// gets or sets the policy name that determines access to the resource.
/// </summary>
string policy { get; set; }
/// <summary>
/// gets or sets a comma delimited list of roles that are allowed to access the resource.
/// </summary>
string roles { get; set; }
/// <summary>
/// gets or sets a comma delimited list of schemes from which user information is constructed.
/// </summary>
string authenticationschemes { get; set; }
}
}

使用 authorizeattribute 不外乎如下几种形式:

1
2
3
4
[authorize]
[authorize("somepolicy")]
[authorize(roles = "角色1,角色2")]
[authorize(authenticationschemes = jwtbearerdefaults.authenticationscheme)]

当然,参数还可以组合起来。另外,roles 和 authenticationschemes 的值以半角逗号分隔,是 or 的关系;多个 authorize 是 and 的关系;policy 、roles 和 authenticationschemes 如果同时使用,也是 and 的关系。

如果要扩展 authorizeattribute,先扩展 iauthorizedata 增加新的属性:

1
2
3
4
5
public interface ipermissionauthorizedata : iauthorizedata
{
string groups { get; set; }
string permissions { get; set; }
}

然后定义 authorizeattribute:

1
2
3
4
5
6
7
8
9
[attributeusage(attributetargets.class | attributetargets.method, allowmultiple = true, inherited = true)]
public class permissionauthorizeattribute : attribute, ipermissionauthorizedata
{
public string policy { get; set; }
public string roles { get; set; }
public string authenticationschemes { get; set; }
public string groups { get; set; }
public string permissions { get; set; }
}

现在,在 controller 或 action 上就可以这样使用了:

1
2
3
[permissionauthorize(roles = "经理,副经理")] // 经理或部门经理
[permissionauthorize(groups = "研发部,生产部", roles = "经理"] // 研发部经理或生成部经理。groups 和 roles 是 `and` 的关系。
[permissionauthorize(groups = "研发部,生产部", roles = "经理", permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。groups 、roles 和 permission 是 `and` 的关系。

数据已经准备好,下一步就是怎么提取出来。通过扩展 authorizationapplicationmodelprovider 来实现。

三、permissionauthorizationapplicationmodelprovider : iapplicationmodelprovider

authorizationapplicationmodelprovider 类的作用是构造 authorizefilter 对象放入 controllermodel 或 actionmodel 的 filters 属性中。具体过程是先提取 controller 和 action 实现了 iauthorizedata 接口的 attribute,如果使用的是默认的defaultauthorizationpolicyprovider,则会先创建一个 authorizationpolicy 对象作为 authorizefilter 构造函数的参数。
创建 authorizationpolicy 对象是由 authorizationpolicy 的静态方法 public static async task<authorizationpolicy> combineasync(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authorizedata) 来完成的。该静态方法会解析 iauthorizedata 的数据,但不懂解析 ipermissionauthorizedata

因为 authorizationapplicationmodelprovider 类对 authorizationpolicy.combineasync 静态方法有依赖,这里不得不做一个类似的 permissionauthorizationapplicationmodelprovider 类,在本类实现 combineasync 方法。暂且不论该方法放在本类是否合适的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
       public static authorizefilter getfilter(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authdata)
{
// the default policy provider will make the same policy for given input, so make it only once.
// this will always execute synchronously.
if (policyprovider.gettype() == typeof(defaultauthorizationpolicyprovider))
{
var policy = combineasync(policyprovider, authdata).getawaiter().getresult();
return new authorizefilter(policy);
}
else
{
return new authorizefilter(policyprovider, authdata);
}
}
private static async task<authorizationpolicy> combineasync(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authorizedata)
{
if (policyprovider == null)
{
throw new argumentnullexception(nameof(policyprovider));
}
if (authorizedata == null)
{
throw new argumentnullexception(nameof(authorizedata));
}
var policybuilder = new authorizationpolicybuilder();
var any = false;
foreach (var authorizedatum in authorizedata)
{
any = true;
var usedefaultpolicy = true;
if (!string.isnullorwhitespace(authorizedatum.policy))
{
var policy = await policyprovider.getpolicyasync(authorizedatum.policy);
if (policy == null)
{
//throw new invalidoperationexception(resources.formatexception_authorizationpolicynotfound(authorizedatum.policy));
throw new invalidoperationexception(nameof(authorizedatum.policy));
}
policybuilder.combine(policy);
usedefaultpolicy = false;
}
var rolessplit = authorizedatum.roles?.split(',');
if (rolessplit != null && rolessplit.any())
{
var trimmedrolessplit = rolessplit.where(r => !string.isnullorwhitespace(r)).select(r => r.trim());
policybuilder.requirerole(trimmedrolessplit);
usedefaultpolicy = false;
}
if(authorizedatum is ipermissionauthorizedata permissionauthorizedatum )
{
var groupssplit = permissionauthorizedatum.groups?.split(',');
if (groupssplit != null && groupssplit.any())
{
var trimmedgroupssplit = groupssplit.where(r => !string.isnullorwhitespace(r)).select(r => r.trim());
policybuilder.requireclaim("group", trimmedgroupssplit); // todo: 注意硬编码
usedefaultpolicy = false;
}
var permissionssplit = permissionauthorizedatum.permissions?.split(',');
if (permissionssplit != null && permissionssplit.any())
{
var trimmedpermissionssplit = permissionssplit.where(r => !string.isnullorwhitespace(r)).select(r => r.trim());
policybuilder.requireclaim("permission", trimmedpermissionssplit);// todo: 注意硬编码
usedefaultpolicy = false;
}
}
var authtypessplit = authorizedatum.authenticationschemes?.split(',');
if (authtypessplit != null && authtypessplit.any())
{
foreach (var authtype in authtypessplit)
{
if (!string.isnullorwhitespace(authtype))
{
policybuilder.authenticationschemes.add(authtype.trim());
}
}
}
if (usedefaultpolicy)
{
policybuilder.combine(await policyprovider.getdefaultpolicyasync());
}
}
return any ? policybuilder.build() : null;
}

if(authorizedatum is ipermissionauthorizedata permissionauthorizedatum ) 为扩展部分。

四、startup

注册 permissionauthorizationapplicationmodelprovider 服务,需要在 addmvc 之后替换掉 authorizationapplicationmodelprovider 服务。

1
2
services.addmvc();
services.replac(servicedescriptor.transient<iapplicationmodelprovider,permissionauthorizationapplicationmodelprovider>());

五、jwt 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[route("api/[controller]")]
[apicontroller]
public class valuescontroller : controllerbase
{
private readonly jwtsecuritytokenhandler _tokenhandler = new jwtsecuritytokenhandler();
[httpget]
[route("signin")]
public async task<actionresult<string>> signin()
{
var user = new claimsprincipal(new claimsidentity(new[]
{
// 备注:claim type: group 和 permission 这里使用的是硬编码,应该定义为类似于 claimtypes.role 的常量;另外,下列模拟数据不一定合逻辑。
new claim(claimtypes.name, "bob"),
new claim(claimtypes.role, "经理"), // 注意:不能使用逗号分隔来达到多个角色的目的,下同。
new claim(claimtypes.role, "副经理"),
new claim("group", "研发部"),
new claim("group", "生产部"),
new claim("permission", "请假审批"),
new claim("permission", "权限1"),
new claim("permission", "权限2"),
}, jwtbearerdefaults.authenticationscheme));
var token = new jwtsecuritytoken(
"signalrauthenticationsample",
"signalrauthenticationsample",
user.claims,
expires: datetime.utcnow.adddays(30),
signingcredentials: signaturehelper.generatesigningcredentials("1234567890123456"));
return _tokenhandler.writetoken(token);
}
[httpget]
[route("test")]
[permissionauthorize(groups = "研发部,生产部", roles = "经理", permissions = "请假审批"] // 研发部经理或生成部经理,并且有请假审批的权限。groups 、roles 和 permission 是 `and` 的关系。
public async task<actionresult<ienumerable<string>>> test()
{
var user = httpcontext.user;
return new string[] { "value1", "value2" };
}
}

六、问题

authorizefilter 类显示实现了 ifilterfactory 接口的 createinstance 方法:

1
2
3
4
5
6
7
8
9
10
11
12
ifiltermetadata ifilterfactory.createinstance(iserviceprovider serviceprovider)
{
if (policy != null || policyprovider != null)
{
// the filter is fully constructed. use the current instance to authorize.
return this;
}

debug.assert(authorizedata != null);
var policyprovider = serviceprovider.getrequiredservice<iauthorizationpolicyprovider>();
return authorizationapplicationmodelprovider.getfilter(policyprovider, authorizedata);
}

竟然对 authorizationapplicationmodelprovider.getfilter 静态方法产生了依赖。庆幸的是,如果通过 authorizefilter(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authorizedata) 或 authorizefilter(authorizationpolicy policy) 创建 authorizefilter 对象不会产生什么不良影响。

七、下一步

[permissionauthorize(groups = "研发部,生产部", roles = "经理", permissions = "请假审批"] 这种形式还是不够灵活,哪怕用多个 attribute, and 和 or 的逻辑组合不一定能满足需求。可以在 ipermissionauthorizedata 新增一个 rule 属性,实现类似的效果:

1
[permissionauthorize(rule = "(groups:研发部,生产部)&&(roles:请假审批||permissions:超级权限)"]

通过 rule 计算复杂的授权。

八、如果通过自定义 iauthorizationpolicyprovider 实现?

另一种方式是自定义 iauthorizationpolicyprovider ,不过还需要自定义 authorizefilter。因为当不是使用 defaultauthorizationpolicyprovider 而是自定义 iauthorizationpolicyprovider 时,authorizationapplicationmodelprovider(或前文定义的 permissionauthorizationapplicationmodelprovider)会使用 authorizefilter(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authorizedata) 创建 authorizefilter 对象,而不是 authorizefilter(authorizationpolicy policy)。这会造成 authorizefilter 对象在 onauthorizationasync 时会间接调用 authorizationpolicy.combineasync 静态方法。

这可以说是一个设计上的缺陷,不应该让 authorizationpolicy.combineasync 静态方法存在,哪怕提供个 iauthorizationpolicycombiner 也好。另外,上文提到的 authorizationapplicationmodelprovider.getfilter 静态方法同样不是一种好的设计。等微软想通吧。

参考资料

 

排版问题: