.NET Core Identity Policy Customize

在上一篇文章中學習了何謂 ClaimPolicy 會員在新增完 Claim 之後可以利用我們事先註冊好的 Policy 能夠避免無權限的使用者呼叫機密的 API

但是在進階的場景,例如:年齡限制或者檢查使用者的性別等需要另外對 Claim 內容進行分辨的邏輯,就需要建立客製化的 Policy

要客製化 Policy 我們先看看官方的範例 Github
Authorization 有建立了四個 Class

  • MinimumAgeAuthorizationHandler.cs
  • MinimumAgeAuthorizeAttribute.cs
  • MinimumAgePolicyProvider.cs
  • MinimumAgeRequirement.cs

並且註冊了 MinimumAgePolicyProviderMinimumAgeAuthorizationHandler 生命週期為 Singleton

AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();

根據註釋的內容可以得知註冊 MinimumAgePolicyProvider 是用來取代預設注入的 DefaultAuthorizationPolicyProvider
PolicyProvider 會根據傳入的 Policy 名稱註冊相對應的 Policy 到系統內

MinimumAgeAuthorizationHandler 則是負責處理此次授權請求為成功或失敗,並且繼承 AuthorizationHandler<MinimumAgeRequirement> 當有 API 需要檢查 Policy 有要求檢查 MinimumAgeRequirement 就會來調用 MinimumAgeAuthorizationHandler

MinimumAgeAuthorizeAttribute 則是用來設定客製化的授權標籤,可有可無

接下來我們可以試著建立自己的 Policy

mkdir Permissions && cd $_
touch MinimumAgeAuthorizationHandler.cs
touch MinimumAgePolicyProvider.cs
touch MinimumAgeRequirement.cs

照流程來看首先 MinimumAgePolicyProvider 會收到一個 PolicyName 我們需要註冊一個 Policy 到系統內,我們在上一篇文章有學到如何註冊 Policy

//Program.cs
builder.Services.AddAuthorization(option =>
{
    option.AddPolicy("birthday", builder =>
    {
        builder
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(IdentityConstants.ApplicationScheme)
            .RequireClaim(ClaimTypes.DateOfBirth);
    });
});

我們也可以手動進行註冊不需要在額外建立 MinimumAgePolicyProvider,這邊我們直接建立一個 Policy 名稱為 MinimumAge18
這邊呼叫 AddRequirements 方法新增一個 MinimumAgeRequirement 傳入一個 Int 我們之後要建立的 MinimumAgeAuthorizationHandler 會接收這個 Requirements 之後進行處理

//Program.cs
option.AddPolicy("MinimumAge18", builder =>
{
    builder.AddRequirements(new MinimumAgeRequirement(18));
});

這裡我們繼承 AuthorizationHandler<MinimumAgeRequirement> 並且透過 Context 讀出目前登入 UserClaim
Claim 轉換為使用這目前的年紀,最後在拿使用者的年紀與 MinimumAgeRequirementInt 進行比較

//MinimumAgeAuthorizationHandler.cs
public class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            return Task.FromResult(0);
        }

        DateTime dateOfBirth =Convert.ToDateTime(context.User?.FindFirst(c => c.Type == ClaimTypes.DateOfBirth)?.Value);
        
        int age = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-age)) age--;

        if (age > requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.FromResult(0);
    }
}
//Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();

這樣基本上就可以用了我們到 WeatherForecastController 進行測試

[Authorize(Policy = "MinimumAge18")]
public class WeatherForecastController : ControllerBase

我們明確指定說 Policy 要使用 MinimumAge18,使用者需要大於 18 歲 API 才會回傳資料


不過這樣使用起來體驗不是很好,我們另外建立 MinimumAgePolicyProviderMinimumAgeAuthorizeAttribute 使用起來會比較方便

這裡建立了一個新標籤可以傳入一個 Int 因為繼承 AuthorizeAttribute 所以我們可以簡化寫法
原本是[Authorize(Policy = "MinimumAge18")] 現在可以改成 [MinimumAgeAuthorize(18)] 可讀性比較高

//MinimumAgeAuthorizeAttribute.cs
public class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
    private const string POLICY_PREFIX = "MinimumAge";

    public MinimumAgeAuthorizeAttribute(int age)
    {
        Age = age;
    }

    public int Age
    {
        get
        {
            if (int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age))
            {
                return age;
            }
    
            return default;
        }
        set
        {
            Policy = $"{POLICY_PREFIX}{value.ToString()}";
        }
    }
}

根據 MinimumAgeAuthorizeAttribute 的內容,我們會跟系統要求一個 MinimumAge18Policy
MinimumAgePolicyProvider 會將數字 18 分離出來,最後添加MinimumAgeRequirement(18)

//MinimumAgePolicyProvider.cs
internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
    const string POLICY_PREFIX = "MinimumAge";

    public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }

    public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
    {
        FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
    }

    public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
            int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
        {
            var policy = new AuthorizationPolicyBuilder();
            policy.AddRequirements(new MinimumAgeRequirement(age));
            return Task.FromResult(policy.Build());
        }

        return Task.FromResult<AuthorizationPolicy>(null);
    }

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

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => Task.FromResult<AuthorizationPolicy?>(null);
}
//Program.cs
builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();

最後我們到 WeatherForecastController 改用我們剛剛建立的標籤,意思明確許多並且也能達到同樣的結果

[MinimumAgeAuthorize(18)]
public class WeatherForecastController : ControllerBase

Summary

今天學習了如何客製化自己的授權邏輯,在某些框架中的授權系統也是使用相同的邏輯來建立出一套複雜的授權系統

今天的進度 Github