.NET Core Identity Authorization
在上一篇的文章中我們使用 .NET Core Identity 的 SignInManager
搭配 Cookie
成功實做出保護 API 的功能
現在我們可以在需要的 API 上添加 [Authorize]
標籤,就能將我們的 API 保護起來只有通過驗證的人可以使用
不過我們沒有在專案設定授權相關的功能,那為什麼我們可以在我們專案直接使用 [Authorize]
標籤?
首先看到底下 WebApplicationBuilder
底下有使用 UseAuthorization
方法,這個是專案建立就有的設定
在 UseAuthorization
方法裡面有一段特殊的邏輯,會去 DI 檢查是否有注入這個服務 AuthorizationPolicyMarkerService
Github
這個服務 AuthorizationPolicyMarkerService
很特殊是專門用來檢查的,服務裡面沒有任何內容 Github
目前只有在使用 AddAuthorization
才會註冊這個服務,所以這個服務是用來確保在系統在註冊 AuthorizationMiddleware
之前,強制檢查有沒有先呼叫 AddAuthorization
方法
不然直接註冊 Middleware 可以會造成錯誤
不過在我們專案中並沒有呼叫 AddAuthorization
方法,怎麼專案沒有報錯呢?
是因為我們開頭有使用 AddControllers
方法,此方法內部已經幫我們使用 AddAuthorization
方法,所以我們才有辦法在不用設定的前提下直接使用 [Authorize]
標籤來保護我們的 API
直接使用 [Authorize]
標籤雖然可以達成我們的需求但是功能比較單一只能進行二分法,也就是只能分成有登入跟沒登入兩種狀態
要進行更複雜的授權之前那就需要先了解什麼是 Claim
與 Policy
Claim
在製作網站通常會有收集會員資料的需求例如會員的生日或電話號碼,以前在關聯式資料庫可能會額外建立一個會員資料表來保存這些資料
不過在 .NET Core 中我們可以利用 Claim 的特性來直接實踐這樣的功能,首先我們先到 Github 上看看原始碼
跟我們之前學習到的 IUserStore
界面與 IUserPasswordStore
密碼相關功能的界面類似
今天我們要看的是 IUserClaimStore
界面,從名稱也可以猜到這是與 Claim 相關的功能界面
我們選一個 AddClaimsAsync
方法來查看,要使用這個方法需要輸入一個 IdentityUser
與輸入列舉的 Claim
Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken);
Claim 內部提供了許多 Constructor,其中最簡單的方式就是輸入兩個 String,分別代表Key:Value
public Claim(string type, string value)
拿開頭的生日作為例子,我們可以建立一個生日的 Claim 來紀錄這個會員的生日
new Claim("birthday","1995-01-01")
這邊有一個問題因為都是 string 所以有可能造成一個生日有多種寫法,value 值也有相同的問題
所以像這種常用的類型我們可以使用微軟提供的 ClaimTypes
與 ClaimValueTypes
裡面已經定義好常用的 Claim
我們可以對生日的 Claim 進行改寫
new Claim(ClaimTypes.DateOfBirth, "1995-01-01", ClaimValueTypes.Date);
有了 Claim 之後現在需要搞懂 AddClaimsAsync
內部是如何把 Claim 跟傳入的 User 連結在一起的
這邊先參考 EFCore 是怎麼實做的,專案直接使用 EFCore 的可以跳過這一步驟 Github
了解到會先利用我們傳入的 user 與 claim 建立出一個新的類型 TUserClaim
//UserStoreBase.cs
protected virtual TUserClaim CreateUserClaim(TUser user, Claim claim)
{
var userClaim = new TUserClaim { UserId = user.Id };
userClaim.InitializeFromClaim(claim);
return userClaim;
}
//IdentityUserClaim.cs
public virtual void InitializeFromClaim(Claim claim)
{
ClaimType = claim.Type;
ClaimValue = claim.Value;
}
正如我們一直在使用的 IdentityUser
Class 這個泛型的 TUserClaim
會繼承另一個預設的 IdentityUserClaim
Class,我們先來看看他的內容 Github
簡化來看這個 Class 只有四個 Property, 並且會與 IdentityUser
的 Id 關聯
int Id
TKey UserId
string? ClaimType
string? ClaimValue
由此可知我們將 Claim 轉換成 IdentityUserClaim
之後會將資料添加資料到 DbSet UserClaims
最後由 EFCore 添加到資料庫內
接下來回到我們的專案進行實做,首先新建立 AspNetUserClaims
表
CREATE TABLE dbo.AspNetUserClaims (
Id integer GENERATED BY DEFAULT AS IDENTITY,
UserId text NOT NULL,
ClaimType text NULL,
ClaimValue text NULL,
CONSTRAINT PK_AspNetUserClaims PRIMARY KEY (Id),
CONSTRAINT FK_AspNetUserClaims_AspNetUsers_UserId FOREIGN KEY (UserId) REFERENCES dbo.AspNetUsers (Id) ON DELETE CASCADE
);
CREATE INDEX IX_AspNetUserClaims_UserId ON dbo.AspNetUserClaims (UserId);
並且到 CustomUserStore
實做新界面 IUserClaimStore<IdentityUser>
Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken);
Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken);
最後到 AccountController
新增幾個 controller,負責幫 user 添加新的 claim 與列出 user 擁有的 claim
HttpGet(template: "~/addClaim", Name = "AddClaim")]
public async Task<IdentityResult> AddClaim()
{
var userName = "User1";
var user = await _userManager.FindByNameAsync(userName);
var claim = new Claim(ClaimTypes.DateOfBirth, "1995-01-01", ClaimValueTypes.Date);
return await _userManager.AddClaimAsync(user, claim);
}
[HttpGet(template: "~/listClaim", Name = "ListClaim")]
public async Task<IList<Claim>> ListClaim()
{
var userName = "User1";
var user = await _userManager.FindByNameAsync(userName);
return await _userManager.GetClaimsAsync(user);
}
[HttpGet(template: "~/listHttpContextClaim", Name = "ListHttpContextClaim")]
public string ListHttpContextClaim()
{
var claims = HttpContext.User.Claims.ToList();
var simpleClaims = claims.Select(claim => new
{
claim = new { claim.Type, claim.Value},
}).ToList() ;
return JsonSerializer.Serialize(simpleClaims);
}
完成之後我們需要先使用 AddClaim
方法內部會利用 UserManager
的 AddClaimAsync
方法會幫 User1 添加新的 Claim
添加之後使用 ListClaim
方法內部會利用 UserManager
的 GetClaimsAsync
列出 User1 擁有的 Claim
比較特別的是 ListHttpContextClaim
方法,內部會利用 HttpContext 取得目前登入 User 身上的 Claim,這是因為使用 signInManager 登入時內部會幫忙把 Claim 添加回 HttpContext
[
{
"claim": {
"Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
"Value": "14da945b-0a45-4240-947b-606b480fc6c2"
}
},
{
"claim": {
"Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"Value": "User1"
}
},
{
"claim": {
"Type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth",
"Value": "1995-01-01"
}
},
{
"claim": {
"Type": "amr",
"Value": "pwd"
}
}
]
Policy
有了 Claim 之後並不能夠給授權系統使用我們還需要搭配 Policy 才能使用 Policy 主要是一組限制規則,例如我們可以建立一個 Policy 要求好幾個規則
- 會員需要通過驗證
- 驗證的規則限定為 Cookie 驗證
- 需要擁有
DateOfBirth
Claim
回到 Program 註冊新的 Policy
builder.Services.AddAuthorization(option =>
{
option.AddPolicy("birthday", builder =>
{
builder
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme)
.RequireClaim(ClaimTypes.DateOfBirth);
});
});
完成後我們就能夠使用 [Authorize]
標籤指定要使用的 Policy 就會要求 Controller 需要符合我們的規則
[Authorize(policy: "birthday")]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
如果條件都符合的話 API 才能正常回傳,否則會回傳 404
Summary
今天學習到了更進階的授權規則,Claim 這個概念之後在使用 JWT 時會常常提到,之後的文章會在討論
今天的進度 Github