8

ASP.NET Core 授权的扩展:使用 IAuthorizationPolicyProvider 的自定义授权策略提供...

 3 years ago
source link: http://blog.tubumu.com/2019/11/06/aspnetcore-extend-authorization-new/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

ASP.NET Core 授权的扩展:使用 IAuthorizationPolicyProvider 的自定义授权策略提供程序

2019-11-062019-11-07ASP.NET Core

01.png

去年写过一篇《ASP.NET Core 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide》,由于在 ASP.NET Core 3 中,Microsoft.AspNetCore.Mvc.Internal 命名空间下的 AuthorizationApplicationModelProvider 类由 public 被改为了 internal,使得无法方便地将其从容器中 DI 容器中移除,所以不得不回到 IAuthorizationPolicyProvider 上来。

ASP.NET Core 提供了基于角色( Role )、声明( Chaim ) 和策略 ( Policy ) 等的授权方式。在实际应用中,可能采用部门( Department , 本文采用用户组 Group )、职位 ( 可继续沿用 Role )、权限( Permission )的方式进行授权。本文通过自定义 IAuthorizationPolicyProvider 进行扩展。

二、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)]

当然,参数还可以组合起来。另外,RolesAuthenticationSchemes 的值以半角逗号分隔,是 Or 的关系;多个 AuthorizeAnd 的关系;PolicyRolesAuthenticationSchemes 如果同时使用,也是 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; }
}

现在,在 ControllerAction 上就可以这样使用了:

1
2
3
[PermissionAuthorize(Roles = "经理,副经理")] // 经理或副经理
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理"] // 研发部或生产部, 或角色是经理。Groups 和 Roles 是 `Or` 的关系。
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生产部经理,或者有请假审批的权限。Groups 、Roles 和 Permissions 是 `Or` 的关系。

备注:这和《ASP.NET Core 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide》一文不同的是,之前 GroupsRolesPermissionsAnd 的关系,而本文是 Or 的关系。原因在于 AuthorizationPolicy.CombineAsync 方法也会用到 Roles,从而达不到 And 的目的。

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

三、PermissionAuthorizeData : IPermissionAuthorizeData

1
2
3
4
5
6
7
8
public class PermissionAuthorizeData : 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; }
}

PermissionAuthorizeDataPermissionAuthorizeAttribute 的唯一区别是,后者是 Attribute

四、PermissionAuthorizationRequirement

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
public class PermissionAuthorizationRequirement : AuthorizationHandler<PermissionAuthorizationRequirement>, IAuthorizationRequirement
{
public PermissionAuthorizeData AuthorizeData { get; }

public PermissionAuthorizationRequirement(PermissionAuthorizeData authorizeData)
{
AuthorizeData = authorizeData;
}

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
if (context.User == null)
{
return Task.CompletedTask;
}

// 以半角逗号分隔的权限满足"需要"的其中之一即可,角色和分组也类似。
// 分组、角色和权限三者在此也是 Or 的关系,所以是在尽力去找任一匹配。
var found = false;
if (requirement.AuthorizeData.Permissions != null)
{
var permissionsClaim = context.User.Claims.FirstOrDefault(c => string.Equals(c.Type, PermissionClaimTypes.Permission, StringComparison.OrdinalIgnoreCase));
if (permissionsClaim?.Value != null && permissionsClaim.Value.Length > 0)
{
var permissionsClaimSplit = SafeSplit(permissionsClaim.Value);
var permissionsDataSplit = SafeSplit(requirement.AuthorizeData.Permissions);
found = permissionsDataSplit.Intersect(permissionsClaimSplit).Any();
}
}

if (!found && requirement.AuthorizeData.Roles != null)
{
var rolesClaim = context.User.Claims.FirstOrDefault(c => string.Equals(c.Type, ClaimTypes.Role, StringComparison.OrdinalIgnoreCase));
if (rolesClaim?.Value != null && rolesClaim.Value.Length > 0)
{
var rolesClaimSplit = SafeSplit(rolesClaim.Value);
var rolesDataSplit = SafeSplit(requirement.AuthorizeData.Roles);
found = rolesDataSplit.Intersect(rolesClaimSplit).Any();
}
}

if (!found && requirement.AuthorizeData.Groups != null)
{
var groupsClaim = context.User.Claims.FirstOrDefault(c => string.Equals(c.Type, PermissionClaimTypes.Group, StringComparison.OrdinalIgnoreCase));
if (groupsClaim?.Value != null && groupsClaim.Value.Length > 0)
{
var groupsClaimSplit = SafeSplit(groupsClaim.Value);
var groupsDataSplit = SafeSplit(requirement.AuthorizeData.Groups);
found = groupsDataSplit.Intersect(groupsClaimSplit).Any();
}
}

if (found)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}

private IEnumerable<string> SafeSplit(string source)
{
return source.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(m => m.Trim()).Where(m => !m.IsNullOrWhiteSpace());
}
}

五、PermissionAuthorizationPolicyProvider : IAuthorizationPolicyProvider

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
public class PermissionAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
const string PolicyPrefix = "Permission:";

public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }

public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
{
// ASP.NET Core only uses one authorization policy provider, so if the custom implementation
// doesn't handle all policies (including default policies, etc.) it should fall back to an
// alternate provider.
//
// In this sample, a default authorization policy provider (constructed with options from the
// dependency injection container) is used if this custom provider isn't able to handle a given
// policy name.
//
// If a custom policy provider is able to handle all expected policy names then, of course, this
// fallback pattern is unnecessary.
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}

public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();

// For ASP.NET Core 3.0
//public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync();

public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase))
{
var policyValue = policyName.Substring(PolicyPrefix.Length);
var authorizeData = JsonConvert.DeserializeObject<PermissionAuthorizeData>(policyValue);
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionAuthorizationRequirement(authorizeData));
return Task.FromResult(policy.Build());
}

// If the policy name doesn't match the format expected by this policy provider,
// try the fallback provider. If no fallback provider is used, this would return
// Task.FromResult<AuthorizationPolicy>(null) instead.
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
}

六、Startup

注册 PermissionAuthorizationPolicyProvider 为单例,以替换内置的 DefaultAuthorizationPolicyProvider

1
services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();

七、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 是 `Or` 的关系。
public async Task<ActionResult<IEnumerable<string>>> Test()
{
var user = HttpContext.User;
return new string[] { "value1", "value2" };
}
}

八、下一步

[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] 这种形式还是不够灵活,哪怕用多个 Attribute, AndOr 的逻辑组合不一定能满足需求。可以在 IPermissionAuthorizeData 新增一个 Rule 属性,实现类似的效果:

1
[PermissionAuthorize(Rule = "(Groups:研发部,生产部)&&(Roles:请假审批||Permissions:超级权限)"]

通过 Rule 计算复杂的授权。

话说,将规则保存在 Policy 名称中,略显丑陋,虽说官方文档在 ASP.NET Core 中使用 IAuthorizationPolicyProvider 的自定义授权策略提供程序也用类似方式实现了个 Sample 。

https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-3.0
https://www.cnblogs.com/RainingNight/p/authorize-how-to-work-in-asp-net-core.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK