ASP.NET Core 中基於策略的授權

風靈使發表於2018-07-23

軟體應用程式的授權層可確保當前使用者能夠訪問指定資源、執行給定操作或對指定資源執行給定操作。在 ASP.NET Core 中,授權層的設定方式有兩種。可以使用角色,也可以使用策略。前一種方法(即基於角色的授權)一直在舊版 ASP.NET 平臺中沿用,而基於策略的授權則是 ASP.NET Core 中新增的方法。

##Authorize 屬性

從早期開始,ASP.NET 應用程式中使用的一直都是角色。從技術角度來講,角色是純字串。不過,它的值被安全層視為元資訊(檢查 IPrincipal 物件中是否有值),並供應用程式使用,用於將一組許可權對映到經過身份驗證的給定使用者。在 ASP.NET 中,登入使用者由 IPrincipal 物件進行標識。在 ASP.NET Core 中,實際類是 ClaimsPrincipal。此類可公開一系列標識,每個標識均由 IIdentity 物件(具體而言,就是 ClaimsIdentity 物件)進行表示。也就是說,任何登入使用者都會隨附一個宣告列表,這其實就是使用者的狀態宣告。使用者名稱和角色是 ASP.NET Core 應用程式使用者的兩個常見宣告。不過,角色是否顯示取決於後備標識儲存區。例如,如果使用社交身份驗證,永遠都不會看到角色。

授權比身份驗證更進一步。身份驗證就是發現使用者標識,而授權則是定義使用者呼叫應用程式終結點的要求。使用者角色通常儲存在資料庫中,並在使用者憑據經過驗證後進行檢索。此時,角色資訊以某種方式附加到使用者帳戶。IIdentity 介面的特徵之一是,必須實現 IsInRole 方法。為此,ClaimsIdentity 類檢查身份驗證程式生成的一系列宣告中是否有角色宣告。總之,當使用者嘗試呼叫安全的控制器方法時,角色應可供檢查。如果不可以,使用者呼叫任何安全的方法時則會遭拒。

Authorize 屬性通過宣告的方式保護控制器或其部分方法:

[Authorize]
public class CustomerController : Controller
{
  ...
}

如果未指定引數,此屬性僅檢查使用者是否經過身份驗證。不過,此屬性支援 Roles 等其他屬性。Roles 屬性指明將對具有任一所列角色的使用者授予訪問許可權。如果需要多個角色,可以多次應用 Authorize 屬性,也可以編寫自己的篩選器。

[Authorize(Roles="admin, system"]
public class BackofficeController : Controller
{
  ...
}

Authorize 屬性還可以視需要通過 ActiveAuthenticationSchemes 屬性,接受一個或多個身份驗證方案。

[Authorize(Roles="admin, system", ActiveAuthenticationSchemes="Cookie"]
public class BackofficeController : Controller
{
  ...
}

ActiveAuthenticationSchemes 屬性是逗號分隔字串,用於列出授權層將在當前上下文中信任的身份驗證中介軟體元件。也就是說,它宣告僅當使用者通過 Cookie 方案進行身份驗證並具有任一所列角色時,才允許訪問 BackofficeController 類。如前所述,傳遞到 ActiveAuthenticationSchemes 屬性的字串值必須與應用程式啟動時註冊的身份驗證中介軟體一致。

請注意,在 ASP.NET 2.0 中,身份驗證中介軟體被替換為包含多個處理程式的服務。因此,身份驗證方案是選擇處理程式的標籤。若要詳細瞭解 ASP.NET Core 中的身份驗證,建議參閱我在 2017 年 9 月發表的專欄文章“ASP.NET Core 中的 Cookie、宣告和身份驗證”(msdn.com/magazine/mt842501)。
授權篩選器

系統提供的授權篩選器使用 Authorize 屬性提供的資訊。此篩選器先於其他任何 ASP.NET Core 篩選器執行,因為它負責檢查使用者能否執行請求的操作。如果使用者未經授權,篩選器會簡化管道,並取消請求。

可以建立自定義授權篩選器,但大部分情況下無需這樣做。實際上,最好配置預設篩選器依賴的現有授權層。

##角色、許可權和否決

藉助角色,可以根據使用者能夠執行或不能執行的操作,對應用程式使用者輕鬆進行分組。不過,這種方法不是非常容易表達;至少,還不足以滿足大部分新式應用程式的需求。例如,假設為相對簡單的授權體系結構,可以服務於網站的普通使用者,以及獲得授權可訪問後端辦公系統軟體並更新內容的 Power User。基於角色的授權層可以圍繞兩個角色(即使用者和管理員)進行構建,這些角色定義了每組可以訪問的控制器和方法。

涉及否決方面的細微區別時,就會遇到問題,因為這些區別描述了具有給定角色的使用者能夠執行或不能執行的操作。例如,可能有使用者喜歡訪問後端辦公系統。但在這些使用者中,有的獲得授權只能編輯客戶資料,有的獲得授權只能處理內容,還有的獲得授權既能編輯客戶資料,也能處理內容(見圖 1)。

角色層次結構
圖 1:角色層次結構

角色實質上是一種平面概念。如何平展圖 1 中所示的簡單層次結構?可以建立四個不同的角色(即 UserAdminCustomerAdminContentsAdmin),但只要否決數量變多,所需的角色數量就會大大增加。即使像這樣的簡單練習,也表明角色可能並不是處理授權的最有效方法(優先考慮向後相容性的簡單方案和例項除外)。對於其他所有情況,要求則不同。下面開始介紹基於策略的授權。

##策略到底是什麼?

ASP.NET Core 中,基於策略的授權框架旨在分離授權與應用程式邏輯。簡而言之,策略是以一系列要求的形式設計的實體,這些要求本身就是當前使用者必須滿足的條件。

最簡單的策略是,對使用者進行身份驗證,同時還須滿足使用者與給定角色相關聯這一常見要求。另一常見要求是,使用者必須有特定宣告或包含某值的特定宣告。從最一般的意義上來講,要求就是斷言了嘗試訪問正確方法的使用者標識。策略物件是使用以下程式碼進行建立:

var policy = new AuthorizationPolicyBuilder()
  .AddAuthenticationSchemes("Cookie, Bearer")
  .RequireAuthenticatedUser()
  .RequireRole("Admin")
  .RequireClaim("editor", "contents")  .RequireClaim("level", "senior")
  .Build();

生成器物件使用各種擴充套件方法收集要求,再生成策略例項。可以看到,要求約束了身份驗證狀態和方案、角色以及通過身份驗證 Cookie 或持有者令牌讀取的任何宣告組合。

如果預定義的所有擴充套件方法都不適用於定義要求,始終可以採取最後手段,即通過自己的斷言定義新要求。命令如下:

var policy = new AuthorizationPolicyBuilder()
  .AddAuthenticationSchemes("Cookie, Bearer")
  .RequireAuthenticatedUser()
  .RequireRole("Admin")
  .RequireAssertion(ctx =>
  {
    return ctx.User.HasClaim("editor", "contents") ||
           ctx.User.HasClaim("level", "senior");
  })
  .Build();

RequireAssertion 方法需要使用 lambda,以接收 HttpContext 物件,並返回布林值。因此,斷言就是條件語句。請注意,如果多次連線 RequireRole,使用者必須履行所有角色。若要改為表達 OR 條件,可能需要將斷言用作最後手段。在此示例中,策略實際上允許角色為內容編輯者或高階使用者的使用者。

##註冊策略

光定義策略還不夠,還必須向授權中介軟體註冊策略。為此,請在 Startup 類的 ConfigureServices 方法中,將授權中介軟體新增為服務,如下所示:

services.AddAuthorization(options=>
{
  options.AddPolicy("ContentsEditor", policy =>
  {
    policy.AddAuthenticationSchemes("Cookie, Bearer");
    policy.RequireAuthenticatedUser();
    policy.RequireRole("Admin");
    policy.RequireClaim("editor", "contents");
  });
}

新增到中介軟體的每個策略都有一個名稱,用於在 Controller 類的 Authorize 屬性中引用策略:

[Authorize(Policy = "ContentsEditor")]
public IActionResult Save(Article article)
{
  // ...
}

使用 Authorize 屬性,可以宣告的方式設定策略,但也可以通過操作方法以程式設計方式呼叫策略,如圖 2 所示。

圖 2:以程式設計方式檢查策略

public class AdminController : Controller
{
  private IAuthorizationService _authorization;
  public AdminController(IAuthorizationService authorizationService)
  {
    _authorization = authorizationService;
  }

  public async Task<IActionResult> Save(Article article)
  {    var allowed = await _authorization.AuthorizeAsync(      User, "ContentsEditor"));
    if (!allowed)
      return new ForbiddenResult();
    
    // Proceed with the method implementation 
    ...
  }
}

如果無法以程式設計方式檢查許可權,建議返回 ForbiddenResult 物件。另一種選擇是,返回 ChallengeResult 物件。在 ASP.NET Core 1.x 中,返回質詢會指示授權中介軟體返回 401 狀態程式碼,或將使用者重定向到登入頁,具體視配置而定。不過,ASP.NET Core 2.0 中不會發生重定向;即使在 ASP.NET Core 1.x 中,如果使用者已登入,質詢最終也會指示返回 ForbiddenResult 物件。最後看來,最好的方法是在無法檢查許可權時返回 ForbiddenResult 物件。

請注意,甚至可以在 Razor 檢視中以程式設計方式檢查策略,如下面的程式碼所示:

@{ 
  var authorized = await Authorization.AuthorizeAsync(
    User, "ContentsEditor"))}
@if (!authorized)
{
  <div class="alert alert-error">
    You’re not authorized to access this page.
  </div>
}

不過,為了讓此程式碼能夠正常執行,必須先注入對授權服務的依賴,如下所示:

@inject IAuthorizationService Authorization

在檢視中使用授權服務,有助於隱藏當前使用者在給定上下文中不得接觸到的 UI 元素。但請注意,光在檢視中隱藏選項還不夠。始終還需要在控制器中強制執行策略。

##自定義要求

常備要求基本上涵蓋了宣告、身份驗證,並提供了常規用途機制,用於根據斷言進行自定義,但也可以建立自定義要求。策略要求由以下兩種元素組成:僅保留資料的要求類,以及對使用者驗證資料的授權處理程式。建立自定義要求,還可以進一步表達特定策略。例如,假設要將內容編輯者策略擴充套件為,增添使用者至少必須有三年經驗的要求。具體程式碼如下:

public class ExperienceRequirement : IAuthorizationRequirement
{
  public int Years { get; private set; }

  public ExperienceRequirement(int minimumYears)
  {
    Years = minimumYears;
  }
}

要求至少必須有一個授權處理程式。處理程式的型別為 AuthorizationHandler<T>,其中 T 是要求型別。圖 3 展示了 ExperienceRequirement 型別的示例處理程式。
圖 3:示例授權處理程式

public class ExperienceHandler : 
             AuthorizationHandler<ExperienceRequirement>
{
  protected override Task HandleRequirementAsync( 
    AuthorizationHandlerContext context, 
    ExperienceRequirement requirement)
  {
    // Save User object to access claims
    var user = context.User;    if (!user.HasClaim(c => c.Type == "EditorSince"))      return Task.CompletedTask;

    var since = user.FindFirst("EditorSince").Value.ToInt();
    if (since >= requirement.Years)
      context.Succeed(requirement);

    return Task.CompletedTask;
  }
}

示例授權處理程式讀取與使用者關聯的宣告,並檢查自定義 EditorSince 宣告。如果找不到,處理程式便無法返回成功。只有在找到宣告且包含的整數值不低於指定年數時,才能返回成功。

自定義宣告應為一條資訊,以某種方式與儲存到身份驗證 Cookie 中的使用者相關聯(例如,“使用者”表中的列)。不過,一旦保留對使用者的引用,便始終可以從宣告中找到使用者名稱,並對任何資料庫或外部服務執行查詢,以獲取經驗年數,從而在處理程式中使用此資訊。(我承認,如果 EditorSince 值保留 DateTime,並計算使用者擔任編輯者是否已有一定年數,此示例會更真實一點。)

授權處理程式呼叫方法 Succeed,同時傳遞當前要求,以通知此要求已成功得到驗證。如果沒有傳遞要求,處理程式無需執行任何操作,可以直接返回內容。不過,如果處理程式要確定是否不符合要求(無論其他處理程式是否已成功驗證同一要求),將會對授權上下文物件呼叫方法 Fail

下面展示瞭如何將自定義要求新增到策略(請注意,由於這是自定義要求,因此沒有擴充套件方法,而必須繼續處理策略物件的整個 Requirements 集合):

services.AddAuthorization(options =>
{
  options.AddPolicy("AtLeast3Years",
    policy => policy
      .Requirements
      .Add(new ExperienceRequirement(3)));
});

此外,還需要在 IAuthorizationHandler 型別的範圍內向 DI 系統註冊新的處理程式:

services.AddSingleton<IAuthorizationHandler, ExperienceHandler>();

如前所述,要求可包含多個處理程式。如果為授權層的同一要求向 DI 系統註冊多個處理程式,有一個成功就足夠了。

##訪問當前 HttpContext

在實現授權處理程式的過程中,可能需要檢查請求屬性或路由資料,如下所示:

if (context.Resource is AuthorizationFilterContext mvc)
{
  var url = mvc.HttpContext.Request.GetDisplayUrl();  ...
}

ASP.NET Core 中,AuthorizationHandlerContext 物件向 FilterContext 物件公開 Resource 屬性集。上下文物件因所涉及的框架而異。例如,MVCSignalR 傳送自己的特定物件。是否發生轉換視需要訪問的內容而定。例如,使用者資訊始終可用,所以無需為此進行轉換;但若要獲取 MVC 專屬詳細資訊(如路由資訊),則需要進行轉換。

##總結

ASP.NET Core 中,授權分為兩種。一種是基於角色的傳統授權,它的工作原理與在經典 ASP.NET MVC 中的工作原理相同,但仍存在相當平面化的結構限制,不適合表達複雜的授權邏輯。基於策略的身份驗證是一種新方法,可提供更豐富、更易表達的模型。這是因為,策略包含一系列基於宣告的要求,以及基於可從 HTTP 上下文或外部源注入的其他任何資訊的自定義邏輯。這些要求各自與一個或多個處理程式相關聯,這些處理程式負責要求的實際計算。

相關文章