接到一個需求是希望能夠針對某些 API 檢查使用者的權限,使用者的權限維護於資料庫中,例如權限名稱叫做 Published
,底下會介紹如何使用 Policy-based authorization
的機制來實現。
環境為 Dotnet core 8
定義 AuthorizationHandler
寫一個 AuthorizationHandler
來處理權限邏輯,預期會在裡面檢查資料庫,確認使用者是否有對應的權限。
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
| public class PermissionRequirement : IAuthorizationRequirement { public string Permission { get; }
public PermissionRequirement(string permission) { Permission = permission; } }
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { private readonly IUserPermissionService _userPermissionService;
public PermissionHandler(IUserPermissionService userPermissionService) { _userPermissionService = userPermissionService; }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == null) { return Task.CompletedTask; }
var hasPermission = await _userPermissionService.UserHasPermissionAsync(userId, requirement.Permission);
if (hasPermission) { context.Succeed(requirement); }
return Task.CompletedTask; } }
|
直接定義 Policy
在 Program.cs
設定 Authorization Policy
,增加一個 RequirePublishedPermission
的規則。
1 2 3 4 5 6 7
| builder.Services.AddAuthorization(options => { options.AddPolicy("PublishedPermission", policy => policy.Requirements.Add(new PermissionRequirement("Published"))); });
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
|
以上處理完畢就能夠在 API 使用 Authorize
這個屬性來做限制。
1 2 3 4 5 6 7 8 9 10 11 12
| [ApiController] [Route("api/[controller]")] public class ArticlesController : ControllerBase { [Authorize(Policy = "PublishedPermission")] [HttpGet] public IActionResult Get() { return Ok(new { Message = "This is a protected API." }); } }
|
動態產生 Policy
上面的作法比較簡單,也相對侷限,必須要在 Program.cs
定義好,另一種作法就是自定義 Policy Provider 來動態的產生 Policy。
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
| public class PermissionPolicyProvider : IAuthorizationPolicyProvider { public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) { FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options); }
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { if (policyName.StartsWith("Permission_", StringComparison.OrdinalIgnoreCase)) { var policy = new AuthorizationPolicyBuilder() .AddRequirements(new PermissionRequirement(policyName.Substring("Permission_".Length))) .Build();
return Task.FromResult(policy); }
return FallbackPolicyProvider.GetPolicyAsync(policyName); }
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync(); }
|
在 Program.cs
註冊
1 2
| builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>(); builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
|
在 API 就能夠透過 Policy Provider 的機制去動態產生前一種作法的 Policy 並且檢查是否有權限。
1 2 3 4 5 6 7 8 9 10 11 12
| [ApiController] [Route("api/[controller]")] public class ArticlesController : ControllerBase { [Authorize(Policy = "Permission_Published")] [HttpGet] public IActionResult Get() { return Ok(new { Message = "This is a protected API." }); } }
|
更好的作法是建立一個 Attribute 用來取代 [Authorize(Policy = "Permission_Published")]
這個寫法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class PermissionAuthorizeAttribute : AuthorizeAttribute { private const string POLICY_PREFIX = "Permission_";
public PermissionAuthorizeAttribute(string rightName) { RightName = rightName; }
public string RightName { get => Policy[POLICY_PREFIX.Length..]; set => Policy = $"{POLICY_PREFIX}{value}"; } }
|
實際使用就可以改成這樣
1
| [PermissionAuthorize("Published")]
|
Reference