ASP.NET Core 授权的扩展:使用 IAuthorizationPolicyProvider 的自定义授权策略提供...
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.
ASP.NET Core 授权的扩展:使用 IAuthorizationPolicyProvider 的自定义授权策略提供程序
2019-11-062019-11-07ASP.NET Core
去年写过一篇《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)]
当然,参数还可以组合起来。另外,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 是 `Or` 的关系。
[PermissionAuthorize(Groups = "研发部,生产部", Roles = "经理", Permissions = "请假审批"] // 研发部经理或生产部经理,或者有请假审批的权限。Groups 、Roles 和 Permissions 是 `Or` 的关系。
备注:这和《ASP.NET Core 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide》一文不同的是,之前
Groups
、Roles
和Permissions
是And
的关系,而本文是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; }
}
PermissionAuthorizeData
和 PermissionAuthorizeAttribute
的唯一区别是,后者是 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
, And
和 Or
的逻辑组合不一定能满足需求。可以在 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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK