.NET Core Identity Policy Customize
在上一篇的文章
中學習了 Claim
與 Policy
的基本知識,會員在新增完 Claim
之後可以搭配我們註冊好的 Policy
就能夠避免不符合規定的使用者呼叫機密的 API
但是在進階的場景,例如:年齡限制或者檢查使用者的性別等需要另外對 Claim
內容進行分辨的邏輯,就需要建立客製化的 Policy
Provider
要客製化 Policy
我們先看看官方的範例 Github
在 Authorization
有建立了四個類別
MinimumAgeAuthorizationHandler.cs
MinimumAgeAuthorizeAttribute.cs
MinimumAgePolicyProvider.cs
MinimumAgeRequirement.cs
首先定義一個新類別實做 IAuthorizationRequirement
介面,之後在使用上只允許傳入 int 代表年齡
internal class MinimumAgeRequirement : IAuthorizationRequirement
{
public int Age { get; private set; }
public MinimumAgeRequirement(int age) { Age = age; }
}
根據下方註釋的內容可以得知 .Net 只允許註冊一個 provider MinimumAgePolicyProvider
所以我們在客製化 provider 的時候要額外提供
Fallback 的 provider,也就是說現在新的 MinimumAgePolicyProvider
已經取代掉 DefaultAuthorizationPolicyProvider
了,
所以未來需要特定 Policy
檢查的流程都會跑 MinimumAgePolicyProvider
的 GetPolicyAsync
方法。
目前只有 policyName 為 MinimumAge
時才會進行年齡檢查
internal class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
const string POLICY_PREFIX = "MinimumAge";
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public MinimumAgePolicyProvider(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();
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync();
// Policies are looked up by string name, so expect 'parameters' (like age)
// to be embedded in the policy names. This is abstracted away from developers
// by the more strongly-typed attributes derived from AuthorizeAttribute
// (like [MinimumAgeAuthorize] in this sample)
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());
}
// 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);
}
}
根據目前掌握的資訊可以得知 Policy 是靠名稱識別的,所以當我們要帶入參數的時候通常會把參數放在 Policy 名稱裡,例如
MinimumAge18
、MinimumAge21
用來表示不同的年齡限制,這時候就可以透過 Attribute 來避免這個問題,
加入 MinimumAgeAuthorizeAttribute
之後就可以改成 MinimumAgeAuthorize(21)
能讓開發者使用上更方便
internal class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
const string POLICY_PREFIX = "MinimumAge";
public MinimumAgeAuthorizeAttribute(int age) => Age = age;
// Get or set the Age property by manipulating the underlying Policy property
public int Age
{
get
{
if (int.TryParse(Policy.AsSpan(POLICY_PREFIX.Length), out var age))
{
return age;
}
return default(int);
}
set
{
Policy = $"{POLICY_PREFIX}{value}";
}
}
}
最後是關鍵的 Handler,在上面的 MinimumAgePolicyProvider
運行中會將 MinimumAgeRequirement
添加到 AuthorizationPolicyBuilder
中,
MinimumAgeAuthorizationHandler
繼承了 AuthorizationHandler<MinimumAgeRequirement>
所以它只會專門處理 MinimumAgeRequirement
接下來讀取會員的 ClaimTypes.DateOfBirth
取得目前會員的生日,並且只有 age >= requirement.Age
才會回傳成功
internal class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;
public MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler> logger)
{
_logger = logger;
}
// Check whether a given MinimumAgeRequirement is satisfied or not for a particular context
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
// Log as a warning so that it's very clear in sample output which authorization policies
// (and requirements/handlers) are in use
_logger.LogWarning("Evaluating authorization requirement for age >= {age}", requirement.Age);
// Check the user's age
var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim != null)
{
// If the user has a date of birth claim, check their age
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value, CultureInfo.InvariantCulture);
var age = DateTime.Now.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Now.AddYears(-age))
{
// Adjust age if the user hasn't had a birthday yet this year
age--;
}
// If the user meets the age criterion, mark the authorization requirement succeeded
if (age >= requirement.Age)
{
_logger.LogInformation("Minimum age authorization requirement {age} satisfied", requirement.Age);
context.Succeed(requirement);
}
else
{
_logger.LogInformation("Current user's DateOfBirth claim ({dateOfBirth}) does not satisfy the minimum age authorization requirement {age}",
dateOfBirthClaim.Value,
requirement.Age);
}
}
else
{
_logger.LogInformation("No DateOfBirth claim present");
}
return Task.CompletedTask;
}
}
接下來我們可以試著建立自己的 Policy
,
首先手動進行註冊不要額外建立 MinimumAgePolicyProvider
,也就是直接註冊一個 Policy
名稱為 MinimumAge18
,
這邊呼叫 AddRequirements
方法新增一個 MinimumAgeRequirement
傳入一個 Int
數值,
我們之後要建立的 MinimumAgeAuthorizationHandler
會接收這個 Requirements
之後進行處理
//Program.cs
option.AddPolicy("MinimumAge18", builder =>
{
builder.AddRequirements(new MinimumAgeRequirement(18));
});
這裡我們繼承 AuthorizationHandler<MinimumAgeRequirement>
並且透過 Context
讀出目前登入 User
的 Claim
將 Claim
轉換為使用這目前的年紀,最後在拿使用者的年紀與 MinimumAgeRequirement
的 Int
進行比較
//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.Age)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
最後註冊 MinimumAgeAuthorizationHandler
到系統中
//Program.cs
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();
這種寫法就是最簡易的,下一步到 AccountController
進行測試
[Authorize(policy: "MinimumAge18")]
[HttpGet("GetUser")]
public async Task<IdentityUser?> GetUser(string userName)
{
return await _userManager.FindByNameAsync(userName);
}
這裡明確指定說 Policy
要使用 MinimumAge18
,使用者需要大於 18 歲 API 才會回傳資料
不過這樣使用起來體驗不是很好,我們另外建立 MinimumAgePolicyProvider
與 MinimumAgeAuthorizeAttribute
使用起來會比較方便
這裡建立了一個 MinimumAgeAuthorizeAttribute
並改成傳入一個 Int
,
原本是[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
的內容,我們會跟系統要求一個 MinimumAge18
的 Policy
MinimumAgePolicyProvider
會將數字 18 分離出來,最後添加MinimumAgeRequirement(18)
//MinimumAgePolicyProvider.cs
public 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)]
[HttpGet("GetUser")]
public async Task<IdentityUser?> GetUser(string userName)
{
return await _userManager.FindByNameAsync(userName);
}
Summary
今天學習了如何客製化自己的授權邏輯,在某些框架中的授權系統也是使用相同的邏輯來建立出一套複雜的授權系統