Policy-based authorization in .NET6

關於Authorization

有二種授權檢驗的方式:Role based 或 Policy based。 Role based 就是用角色決定,俱體上設定 Role 屬性在目標上,這會綁定固較沒有彈性難以客製化。可適於小型系統。 Policy based 是所謂抽像的〝政策〞,在俱體上就是跑程式決定。較有彈性然實作上有點小麻煩。

Policy based 關鍵知識

  1. 為『授權標的』指定『Policy』。

  2. 為該『Policy』註冊須滿足的授權需求『IAuthorizationRequirement』。

  3. 滿足這些授權需求『IAuthorizationRequirement』就滿足『Policy』。

將 Policy based 關聯簡化表示如下

Target ---> Policy ---> Requirement ---> Handler

為授權標的指定 Policy

[ApiController]
[Route("api/[controller]/[action]")]
[Authorize("AuthFunc")] //<--- 指定 Policy based authorization
[AuthFunc("DEMO04")]    //<--- 添加客製屬性:表明標的的FuncCode。
public class YourController : ControllerBase  //<--- 授權標的
{
  ...省略...
}

為 Policy 註冊須滿足的授權需求(Authorization Requirement)

Program.cs
... 省略 ...

builder.Services.AddAuthentication(/*** 省略 ***/);

// 註冊 客製 Policy 與滿足它所要的需求 Authorization Requirement
// 一個 Policy 可以滿足多個需求才成立,此例只有一個:AuthFunc。
builder.Services.AddAuthorization(options => {
  options.AddPolicy("AuthFunc", policy => policy.Requirements.Add(new AuthFuncRequirement()));
});

// 註冊 客製化授權需求 Authorization Requirement。
// ※ 將自動觸發對應的檢驗程序 
builder.Services.AddSingleton<IAuthorizationHandler, AuthFuncHandler>();

// 註冊 客製化服務 --- 認證與授權
builder.Services.AddSingleton<AccountService>();

var app = builder.Build();

... 省略 ...
app.UseAuthentication();
app.UseAuthorization();
... 省略 ...

滿足授權需求(Authorization Requirement)就滿足 Policy

AuthFuncRequirement.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Diagnostics;
using System.Reflection;
using System.Security.Claims;
using YourProject.Controllers;
using YourProject.Services;

namespace YourProject.Models;

// 用來添加客製屬性:表明標的的FuncCode。用於授權檢驗需求。
// ※ 不必實作內容,只做為需求參數的取得來源。
[AttributeUsage(AttributeTargets.Class)]
public class AuthFuncAttribute : Attribute
{
  public AuthFuncAttribute(string funcCode) { }
}

// 宣告 Authorization Requirement
// ※ 不必實作內容,只做為需求的宣示。
class AuthFuncRequirement : IAuthorizationRequirement { }

// 實作 handler --- Authorization Requirement 檢驗程序
internal class AuthFuncHandler : AuthorizationHandler<AuthFuncRequirement>
{
  readonly ILogger<AuthFuncHandler> _logger;
  readonly AccountService _account;

  // 將注入資源
  public AuthFuncHandler(ILogger<AuthFuncHandler> logger, AccountService account)
  {
    _logger = logger;
    _account = account;
  }

  // 檢驗程序實作
  protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthFuncRequirement requirement)
  {
    //# 未登入離開。
    if (!context.User.Identity?.IsAuthenticated ?? false)
      return Task.CompletedTask;

    //# 取得需要資源,若非預期資源離開。
    var http = context.Resource as DefaultHttpContext;
    if (http == null) return Task.CompletedTask;
    var endpoint = http.GetEndpoint();
    if (endpoint == null) return Task.CompletedTask;
    var descriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    if (descriptor == null) return Task.CompletedTask;

    //# 取得客製資源
    // 功能代碼:預設為Controller名稱。 
    string? funcCode = descriptor.ControllerName; 
    // 但若用 AuthFuncAttribute 指定[功能代碼]則為優先。 
    CustomAttributeData? authFunc = descriptor.ControllerTypeInfo.CustomAttributes.FirstOrDefault(c => c.AttributeType == typeof(AuthFuncAttribute));
    if (authFunc != null && authFunc.ConstructorArguments.Count > 0)
      funcCode = authFunc.ConstructorArguments[0].Value as string;

    //# 開始驗證
    // 是否登入者有授權的功能清單。
    if(!InAuthFuncList(context.User, funcCode)) return Task.CompletedTask;

    // OK
    context.Succeed(requirement); // 滿足此項授權需求
    return Task.CompletedTask;
  }

  /// <summary>
  /// help funciton: 有否在登入者的授權功能清單中
  /// </summary>
  bool InAuthFuncList(ClaimsPrincipal user, string? funcCode)
  {
    if (funcCode == null) return false;

    // 檢查是否有該授權功能
    AuthUser? auth = _account.GetSessionUser(user.Identity);
    if (auth == null) return false;
    if(!auth.AuthFuncList().Contains(funcCode)) return false;

    // Success
    return true;
  }
}

完整程式碼

參考文章

Last updated