AspNetCore3.1_Secutiry源碼解析_8_Authorization_授權框架

  • 2020 年 3 月 26 日
  • 筆記

目錄

簡介

開篇提到過,認證主要解決的是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的就不細說了,感興趣可以翻看源碼。
AspNetCoresrcMvcMvc.CoresrcApplicationModelsAuthorizationApplicationModelProvider.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());              }          }      }  }