当前位置: 移动技术网 > IT编程>开发语言>.net > ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide

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

2018年11月29日  | 移动技术网IT编程  | 我要评论

古巴英雄,中国女排最新消息,赢在中国2013

一、概述

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 静态方法同样不是一种好的设计。等微软想通吧。

参考资料

 

排版问题:

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网