[WebAPI] 權限管理:透過AuthorizationFilterAttribute來客製化權限管理
在上一篇的文章介紹裡已經提到如何使用WebAPI內建的權限管理機制來檢查使用者權限,但當我們不使用角色或是使用者名稱,而改成用其他資訊例如Header裡的夾帶的「API Key」鍵值時該如何客製化符合自己需求的權限檢查邏輯呢?這時候我們可以透過繼承「AuthorizationFilterAttribute」標籤來處理。
開發一個客製化的authorization filter我們可以透過繼承以下兩個類別或是實作一個介面來完成:
- AuthorizeAttribute: 繼承此類別並透過使用者身分與角色來實作權限管理
- AuthorizationFilterAttribute: 繼承此類別並透過自訂資訊來實作權限管理
- IAuthorizationFilter: 實作此介面來執行非同步的權限檢查邏輯,例如當你的權限邏輯需要使用非同步來檢查
上述提到的類別與介面可透過以下的階層圖來瞭解其關係:
實作方式分成四個部分:
- CustomMessageHandler.cs
- CustomAuthorizationFilter.cs
- 在WebApiConfig.cs裡註冊CustomMessageHandler
- 在ValuesController的Action方法上掛載「CustomAuthorizationFilter」標籤
Step 1. CustomMessageHandler.cs
- 目的:我們需要一個自訂的MessageHandler幫我們把產生一個IPrincipal型別的物件並且塞到Thread.CurrentPrincipal的屬性裡。
- 流程:先檢查Header裡的「APIKey」欄位是否存在,如存在且鍵值等於「TestApiKey」時,產生一個使用者名稱為「Kevin」的GerericPrincipal物件並塞到Thread.CurrentPrincipal裡面。
using System; using System.Linq; using System.Net.Http; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using System.Web; namespace MyWebAPI.MessageHandlers { public class CustomMessageHandler : DelegatingHandler { /// <summary> /// Header名稱預設為「APIKey」 /// </summary> private const string _header = "APIKey"; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //// 檢查request裡是否有「APIKey」這個Header var apiKey = Enumerable.Empty<string>(); bool isHeaderExist = request.Headers.TryGetValues(_header, out apiKey); //// 如果有「APIKey」這個Header且APIKey = "TestApiKey" if (isHeaderExist && string.Compare(apiKey.FirstOrDefault(), "TestApiKey", true) == 0) { this.SetPrincipal(); } return base.SendAsync(request, cancellationToken); } /// <summary> /// 設定IPrincipal /// </summary> private void SetPrincipal() { //// 設定使用者識別 => 就是使用者名稱啦 //// GenericIdentity.IsAuthenticated 預設為true GenericIdentity identity = new GenericIdentity("Kevin"); //// 設定使用者所屬的群組 String[] mMyStringArray = { "RD" }; //// 將使用者的識別與其所屬群組設定到GenericPrincipal類別上 GenericPrincipal principal = new GenericPrincipal(identity, mMyStringArray); Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } } } }
Step 2. CustomAuthorizationFilter.cs
- 目的:繼承AuthorizationFilterAttribute類別,並透過覆寫OnAuthorization的方法來實作自訂的權限檢查邏輯。
- 流程:將IPrincipal物件取出來,透過IAuthorizationService物件讓context端自己決定要如何使用IPrincipal裡的資訊來實現權限驗證。
using System.Net; using System.Net.Http; using System.Security.Principal; using System.Threading; using System.Web.Http.Filters; namespace MyWebAPI.Filters { public class CustomAuthorizationFilter : AuthorizationFilterAttribute { /// <summary> /// The authorization service /// </summary> private IAuthorizationService authorizationService = new CustomAuthorizationService(); /// <summary> /// The authenticated username /// </summary> private const string authenticatedUsername = "Kevin"; /// <summary> /// 在處理序要求授權時呼叫。 /// </summary> /// <param name="actionContext">動作內容,該內容封裝 <see cref="T:System.Web.Http.Filters.AuthorizationFilterAttribute" /> 的使用資訊。</param> public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext) { bool isAuthorizated = false; //// 從Thread取出IPrincipal IPrincipal principal = Thread.CurrentPrincipal; isAuthorizated = authorizationService.IsAuthorizated(principal); if (!isAuthorizated) { //// CreateResponse是System.Net.Http命名空間的擴充方法 actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized); } } } /// <summary> /// IAuthorizationService /// </summary> public interface IAuthorizationService { /// <summary> /// 檢查該使用者的名稱是否有權限 /// </summary> bool IsAuthorizated(IPrincipal principal); } /// <summary> /// CustomAuthorizationService /// </summary> public class CustomAuthorizationService : IAuthorizationService { public bool IsAuthorizated(IPrincipal principal) { //// 這邊context端需要實作自訂的權限檢查邏輯,範例裡透過檢查Identity.Name是否等於「Kevin」來判斷 bool isIdentityAuthenticated = principal.Identity.IsAuthenticated; bool isUsernameCorrect = principal.Identity.Name == "Kevin"; return isIdentityAuthenticated && isUsernameCorrect; } } }
Step 3. 在WebApiConfig.cs裡註冊CustomMessageHandler
using System.Web.Http;
using MyWebAPI.MessageHandlers;
namespace MyWebAPI
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//// 註冊CustomMessageHandler
config.MessageHandlers.Add(new CustomMessageHandler());
// 取消註解以下程式碼行以啟用透過 IQueryable 或 IQueryable<T> 傳回類型的動作查詢支援。
// 為了避免處理未預期或惡意佇列,請使用 QueryableAttribute 中的驗證設定來驗證傳入的查詢。
// 如需詳細資訊,請造訪 http://go.microsoft.com/fwlink/?LinkId=279712。
//config.EnableQuerySupport();
// 若要停用您應用程式中的追蹤,請將下列程式碼行標記為註解或加以移除
// 如需詳細資訊,請參閱: http://www.asp.net/web-api
config.EnableSystemDiagnosticsTracing();
}
}
}
Step 4. 在ValuesController的Action方法上掛載「CustomAuthorizationFilter」標籤
using System.Collections.Generic;
using System.Web.Http;
using MyWebAPI.Filters;
namespace MyWebAPI.Controllers
{
public class ValuesController : ApiController
{
[CustomAuthorizationFilter]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
}
注意事項:
- 當Http Request進入WebAPI的管線時,Thread.CurrentPrincipal.Identity物件就已存在,且Thread.CurrentPrincipal.Identity.Name為空字串。
- 當我們在覆寫OnAuthorization方法的時候,透過ActionContext.Request來回傳HttpResponseMessage時要記得加上「System.Net.Http」的命名空間,這麼一來我們才可以使用CreateResponse的擴充方法。