AspNetCore3.1_Secutiry原始碼解析_8_Authorization_授權框架
目錄
- AspNetCore3.1_Secutiry原始碼解析_1_目錄
- AspNetCore3.1_Secutiry原始碼解析_2_Authentication_核心流程
- AspNetCore3.1_Secutiry原始碼解析_3_Authentication_Cookies
- AspNetCore3.1_Secutiry原始碼解析_4_Authentication_JwtBear
- AspNetCore3.1_Secutiry原始碼解析_5_Authentication_OAuth
- AspNetCore3.1_Secutiry原始碼解析_6_Authentication_OpenIdConnect
- AspNetCore3.1_Secutiry原始碼解析_7_Authentication_其他
- AspNetCore3.1_Secutiry原始碼解析_8_Authorization_授權框架
簡介
開篇提到過,認證主要解決的是who are you,授權解決的是 are you allowed的問題。各種認證架構可以幫我們知道使用者身份(claims),oauth等架構的scope欄位能夠控制api服務級別的訪問許可權,但是更加細化和多變的功能授權不是它們的處理範圍。
微軟的Authorization專案提供了基於策略的靈活的授權框架。
推薦看下面部落格瞭解,我主要學習和梳理原始碼。
https://www.cnblogs.com/RainingNight/p/authorization-in-asp-net-core.html
依賴注入
注入了以下介面,提供了預設實現
- IAuthorizationService :授權服務,主幹服務
- IAuthorizationPolicyProvider : 策略提供類
- IAuthorizationHandlerProvider:處理器提供類
- IAuthorizationEvaluator:校驗類
- IAuthorizationHandlerContextFactory:授權上下文工廠
- IAuthorizationHandler:授權處理器,這個是注入的集合,一個策略可以有多個授權處理器,依次執行
- 配置類:AuthorizationOptions
微軟的命名風格還是比較一致的
Service:服務
Provider:某類的提供者
Evaluator:校驗預處理類
Factory:工廠
Handler:處理器
Context:上下文
看原始碼的過程,不僅可以學習框架背後原理,還可以學習編碼風格和設計模式,還是挺有用處的。
/// <summary>
/// Adds authorization services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddAuthorizationCore(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>());
services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>());
return services;
}
/// <summary>
/// Adds authorization services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="configure">An action delegate to configure the provided <see cref="AuthorizationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddAuthorizationCore(this IServiceCollection services, Action<AuthorizationOptions> configure)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure != null)
{
services.Configure(configure);
}
return services.AddAuthorizationCore();
}
配置類 - AuthorizationOptions
- PolicyMap:策略名稱&策略的字典資料
- InvokeHandlersAfterFailure: 授權處理器失敗後是否觸發下一個處理器,預設true
- DefaultPolicy:預設策略,構造了一個RequireAuthenticatedUser策略,即需要認證使用者,不允許匿名訪問。現在有點線索了,為什麼api一加上[Authorize],就會校驗授權。
- FallbackPolicy:保底策略。沒有任何策略的時候會使用保底策略。感覺有點多此一舉,不是給了個預設策略嗎?
- AddPolicy:新增策略
- GetPolicy:獲取策略
/// <summary>
/// Provides programmatic configuration used by <see cref="IAuthorizationService"/> and <see cref="IAuthorizationPolicyProvider"/>.
/// </summary>
public class AuthorizationOptions
{
private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Determines whether authentication handlers should be invoked after a failure.
/// Defaults to true.
/// </summary>
public bool InvokeHandlersAfterFailure { get; set; } = true;
/// <summary>
/// Gets or sets the default authorization policy. Defaults to require authenticated users.
/// </summary>
/// <remarks>
/// The default policy used when evaluating <see cref="IAuthorizeData"/> with no policy name specified.
/// </remarks>
public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
/// <summary>
/// Gets or sets the fallback authorization policy used by <see cref="AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider, IEnumerable{IAuthorizeData})"/>
/// when no IAuthorizeData have been provided. As a result, the AuthorizationMiddleware uses the fallback policy
/// if there are no <see cref="IAuthorizeData"/> instances for a resource. If a resource has any <see cref="IAuthorizeData"/>
/// then they are evaluated instead of the fallback policy. By default the fallback policy is null, and usually will have no
/// effect unless you have the AuthorizationMiddleware in your pipeline. It is not used in any way by the
/// default <see cref="IAuthorizationService"/>.
/// </summary>
public AuthorizationPolicy FallbackPolicy { get; set; }
/// <summary>
/// Add an authorization policy with the provided name.
/// </summary>
/// <param name="name">The name of the policy.</param>
/// <param name="policy">The authorization policy.</param>
public void AddPolicy(string name, AuthorizationPolicy policy)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}
PolicyMap[name] = policy;
}
/// <summary>
/// Add a policy that is built from a delegate with the provided name.
/// </summary>
/// <param name="name">The name of the policy.</param>
/// <param name="configurePolicy">The delegate that will be used to build the policy.</param>
public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (configurePolicy == null)
{
throw new ArgumentNullException(nameof(configurePolicy));
}
var policyBuilder = new AuthorizationPolicyBuilder();
configurePolicy(policyBuilder);
PolicyMap[name] = policyBuilder.Build();
}
/// <summary>
/// Returns the policy for the specified name, or null if a policy with the name does not exist.
/// </summary>
/// <param name="name">The name of the policy to return.</param>
/// <returns>The policy for the specified name, or null if a policy with the name does not exist.</returns>
public AuthorizationPolicy GetPolicy(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null;
}
}
IAuthorizationService - 授權服務 - 主幹邏輯
介面定義了授權方法,有兩個過載,一個是基於requirements校驗,一個是基於policyName校驗。
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName);
看下預設實現DefaultAuthorizationService的處理,邏輯還是比較簡單
- 獲取策略
- 獲取策略的授權條件
- 獲取授權上下文
- 獲取處理器集合
- 處理器依次執行,結果存入上下文
- 校驗器驗證上下文
- 返回授權結果類
/// <summary>
/// The default implementation of an <see cref="IAuthorizationService"/>.
/// </summary>
public class DefaultAuthorizationService : IAuthorizationService
{
private readonly AuthorizationOptions _options;
private readonly IAuthorizationHandlerContextFactory _contextFactory;
private readonly IAuthorizationHandlerProvider _handlers;
private readonly IAuthorizationEvaluator _evaluator;
private readonly IAuthorizationPolicyProvider _policyProvider;
private readonly ILogger _logger;
/// <summary>
/// Creates a new instance of <see cref="DefaultAuthorizationService"/>.
/// </summary>
/// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/> used to provide policies.</param>
/// <param name="handlers">The handlers used to fulfill <see cref="IAuthorizationRequirement"/>s.</param>
/// <param name="logger">The logger used to log messages, warnings and errors.</param>
/// <param name="contextFactory">The <see cref="IAuthorizationHandlerContextFactory"/> used to create the context to handle the authorization.</param>
/// <param name="evaluator">The <see cref="IAuthorizationEvaluator"/> used to determine if authorization was successful.</param>
/// <param name="options">The <see cref="AuthorizationOptions"/> used.</param>
public DefaultAuthorizationService(IAuthorizationPolicyProvider policyProvider, IAuthorizationHandlerProvider handlers, ILogger<DefaultAuthorizationService> logger, IAuthorizationHandlerContextFactory contextFactory, IAuthorizationEvaluator evaluator, IOptions<AuthorizationOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (policyProvider == null)
{
throw new ArgumentNullException(nameof(policyProvider));
}
if (handlers == null)
{
throw new ArgumentNullException(nameof(handlers));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (contextFactory == null)
{
throw new ArgumentNullException(nameof(contextFactory));
}
if (evaluator == null)
{
throw new ArgumentNullException(nameof(evaluator));
}
_options = options.Value;
_handlers = handlers;
_policyProvider = policyProvider;
_logger = logger;
_evaluator = evaluator;
_contextFactory = contextFactory;
}
/// <summary>
/// Checks if a user meets a specific set of requirements for the specified resource.
/// </summary>
/// <param name="user">The user to evaluate the requirements against.</param>
/// <param name="resource">The resource to evaluate the requirements against.</param>
/// <param name="requirements">The requirements to evaluate.</param>
/// <returns>
/// A flag indicating whether authorization has succeeded.
/// This value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>.
/// </returns>
public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
if (requirements == null)
{
throw new ArgumentNullException(nameof(requirements));
}
var authContext = _contextFactory.CreateContext(requirements, user, resource);
var handlers = await _handlers.GetHandlersAsync(authContext);
foreach (var handler in handlers)
{
await handler.HandleAsync(authContext);
if (!_options.InvokeHandlersAfterFailure && authContext.HasFailed)
{
break;
}
}
var result = _evaluator.Evaluate(authContext);
if (result.Succeeded)
{
_logger.UserAuthorizationSucceeded();
}
else
{
_logger.UserAuthorizationFailed();
}
return result;
}
/// <summary>
/// Checks if a user meets a specific authorization policy.
/// </summary>
/// <param name="user">The user to check the policy against.</param>
/// <param name="resource">The resource the policy should be checked with.</param>
/// <param name="policyName">The name of the policy to check against a specific context.</param>
/// <returns>
/// A flag indicating whether authorization has succeeded.
/// This value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>.
/// </returns>
public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
{
if (policyName == null)
{
throw new ArgumentNullException(nameof(policyName));
}
var policy = await _policyProvider.GetPolicyAsync(policyName);
if (policy == null)
{
throw new InvalidOperationException($"No policy found: {policyName}.");
}
return await this.AuthorizeAsync(user, resource, policy);
}
}
預設策略 - 需要認證使用者
預設策略添加了校驗條件DenyAnonymousAuthorizationRequirement
public AuthorizationPolicyBuilder RequireAuthenticatedUser()
{
Requirements.Add(new DenyAnonymousAuthorizationRequirement());
return this;
}
校驗上下文中是否存在認證使用者資訊,驗證通過則在上下文中將校驗條件標記為成功。
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement)
{
var user = context.User;
var userIsAnonymous =
user?.Identity == null ||
!user.Identities.Any(i => i.IsAuthenticated);
if (!userIsAnonymous)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
授權時序圖
授權專案還是比較好理解的,微軟提供了一個基於策略的授權模型,大部門的具體的業務程式碼還是需要自己去實現的。
classDiagram
class AuthorizationPolicy{
Requirements
}
class Requirement{
}
class AuthorizationHandler{
}
class IAuthorizationHandler{
+HandleAsync(AuthorizationHandlerContext context)
}
class IAuthorizationRequirement{
}
Requirement-->AuthorizationHandler
AuthorizationHandler-->IAuthorizationHandler
Requirement-->IAuthorizationHandler
Requirement-->IAuthorizationRequirement
中介軟體去哪了?
開發不需要編寫UseAuthorization類似程式碼,專案中也沒發現中介軟體,甚至找不到 使用AuthorizeAttribute的地方。那麼問題來了,框架怎麼知道某個方法標記了[Authorize]特性,然後執行校驗的呢?
答案是Mvc框架處理的,它讀取了節點的[Authorize]和[AllowAnonymous]特性,並觸發相應的邏輯。關於Mvc的就不細說了,感興趣可以翻看原始碼。
AspNetCore\src\Mvc\Mvc.Core\src\ApplicationModels\AuthorizationApplicationModelProvider.cs。
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (_mvcOptions.EnableEndpointRouting)
{
// When using endpoint routing, the AuthorizationMiddleware does the work that Auth filters would otherwise perform.
// Consequently we do not need to convert authorization attributes to filters.
return;
}
foreach (var controllerModel in context.Result.Controllers)
{
var controllerModelAuthData = controllerModel.Attributes.OfType<IAuthorizeData>().ToArray();
if (controllerModelAuthData.Length > 0)
{
controllerModel.Filters.Add(GetFilter(_policyProvider, controllerModelAuthData));
}
foreach (var attribute in controllerModel.Attributes.OfType<IAllowAnonymous>())
{
controllerModel.Filters.Add(new AllowAnonymousFilter());
}
foreach (var actionModel in controllerModel.Actions)
{
var actionModelAuthData = actionModel.Attributes.OfType<IAuthorizeData>().ToArray();
if (actionModelAuthData.Length > 0)
{
actionModel.Filters.Add(GetFilter(_policyProvider, actionModelAuthData));
}
foreach (var attribute in actionModel.Attributes.OfType<IAllowAnonymous>())
{
actionModel.Filters.Add(new AllowAnonymousFilter());
}
}
}
}