一、概述
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
静态方法同样不是一种好的设计。等微软想通吧。
参考资料
排版问题: