在開始之前我們得搞清楚這兩者的區別. 認證是我們在訪問某數據資源的時候, 需要提供一個身份identity, 然后server拿着這個identity, 去某個存儲容器中去匹配, 如果匹配上了, 證明認證成功.
至於是否你有權限訪問這個資源, 需要看是否你對這個資源有權限, 想獲取權限, 就必須給你的identity授權, 也就是讓你有權限去訪問資源.所以兩個動作描述的階段時不一樣的.
所以簡單點來說, 這兩者一結合, 就相當於訪問者訪問 web server資源的一個過程. 首先訪問者得持有一個login user, 用於登錄web server. 然后web server這面也會持有一個訪問者清單. 只有當
login user與清單中的user相匹配才能訪問web server. 但是這個login user如果想訪問的資源必須得到相應的權限級別. 有的機密文件則需要申請, 得到admin的 approve. 這個過程叫授權.
關於 Authentication
services.AddAuthentication
可以在 startup.cs 中的 configureservice 方法內部注入 IAuthenticationService 中間件. 這個 Authentication service 會使用注冊到程序的 Authentication handler 進行相應的認證邏輯. 這些注冊的 Authentication handelers 被稱為 schemas. 所以我們通常見在 Startup.ConfigureServices
見到這樣的配置:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options)) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options);
此處有多個 schema 被注冊了進來. 所以在后面進行身份認證時, 就可以根據實際需要, 去使用不同的 schema 去進行認證. 使用方式也很簡單, 比如對某個 Controller 使用 jwt 認證:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class MixedController : Controller
具體可以參考微軟文檔:
Authorize with a specific scheme in ASP.NET Core
services.AddDefaultIdentity
如果 asp.net core 項目是 web MVC 項目, 並且搭配了 individual users 模板, 我們可能會在 startup.configureservice 中看到這樣的注入行為:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddRazorPages();
而 AddAuthentication
實際上是內置在 AddDefaultIdentity
里面了. 所以不用額外去再加一次.
app.UseAuthentication();
無論項目使用哪種方式將 Authentication Service 注入進來, 都需要在 startup.configure 中執行:
app.UseRouting()
app.UseAuthentication();
aspp.UseEndpoints();
將其添加到 http 請求響應的管道中去. 並且配置的相對順序不能變.
Authentication 相關的術語
- Schema
一般是根據你添加的認證方式, 以及配置的 options, 去對 http request 請求進行身份認證. 所以
AddJwtBearer
, 就是添加了 jwt 認證方式,AddCookie
其實就是添加了 cookie 認證.
- Challenge
這里是指認證的過程, 比如當匿名用戶請求登錄或點擊受限的資源鏈接。Authentication Service 會根據相應的或者默認的 Schema 進行認證的過程。 通常情況下, 基於 cookie 的認證會將用戶重定向到 login 界面. 而基於 jwt 的認證, 則會返回 401 的 code.
- Forbid
Forbid 發生在身份認證通過以后的鑒權階段, 由 Authorization service 判定 user 是否有權限訪問資源. 當用戶無權限訪問資源時, 基於 cookie 的認證會在此階段將用戶重定向到一個顯示 'user 無權限訪問 ' 的page. 而基於 jwt 的認證會返回一個 403 code.
關於 Authorization
Authorization 機制是必須在 Authentication 結合下才能工作的. 現在我們需要簡單了解一些關於鑒權的一些基本知識.
app.UseAuthorization();
我們通常會看到在 startup.configure 中, 會有一段代碼:
app.UseAuthorization();
然后在某個 Controller 或者 Action 會看到:
[ApiController] [Route("api/[controller]")] [Authorize] public class PostController : DennisController {
然后這個 Controller 和 Action 就是一個不允許匿名訪問的資源. 也就是說 http 請求如果想訪問這個 api, 需要攜帶認證信息, 經過身份認證后才能通過. 這一過程被稱為簡單鑒權
Simple authorization in ASP.NET Core
簡單鑒權不能為我們系統提供細分領域的權限划分.
基於 role 的鑒權
當用戶在系統的賬戶中心注冊了一個賬戶后, 除了賬戶 id 和 password 之外, 一般還會為這個用戶加入到系統的某個權限組中, 比如是 Admin, Manager, Member 等等. 不同的角色可以訪問的資源也不同. 比如 小明注冊了賬戶, 然后分配的權限是 Manager:
訪問 get action 的權限是 ContactAdministrators. 我們分別用以下兩個賬戶登錄系統后訪問該資源.
使用 manager 用戶訪問:
可以看到訪問的 api/accounts/ 資源, 但是返回的結果是 access denied page, code 為 200.
而使用 admin 用戶登錄, 身份認證通過后, 鑒權也得以通過, 可以得到 api 中的資源內容:
更多關於 role-based 的鑒權和設定, 可以參考微軟文檔: Role-based authorization in ASP.NET Core
基於 claims 的鑒權
當一個身份證(identity)被創建出來后, 一般會由發證機關(identity server)對這個身份證添加一些條目(claims)來表明這個身份證的身份, 比如姓名, 性別, 出生年月, 身份證 id 等等. 這些條目組合到一起成為你的身份信息.
基於 claims 的鑒權例子也很多, 比如買 CBA 球票的時候, 球迷需要出示身份證買票, 如果發現你的身份證屬於客隊家鄉的所在地(claim), 則只允許你買座位會被限定於球場的客隊球迷的座位的票. 再比如在國外買酒的時候需要出示身份證, 驗證你的身份后, 還需看你的年齡(claim)是否超過18歲, 才能合法買酒等等.
但是這種基於 claims 的鑒權策略一般是受到政策限制的. 就拿上面的例子來說, 如果沒有這種政策, 則不會有這種鑒權行為, 所以在 asp.net core 程序中如果使用 claims 鑒權, 也需要在 startup.configureservice 中注冊 policy.
更多關於 claims-based 的鑒權, 可參考微軟文檔: Claims-based authorization in ASP.NET Core
基於 policy 的鑒權
說完上面兩種鑒權方式后, 繼續說明, 當 asp.net core 程序中需要定制一種策略(policy) 去作為程序的鑒權原則時, 往往需要用到這個小節提到的基於 policy 的鑒權. 首先我們看看怎么添加 policy 鑒權的代碼示例, 在 startup.configureservice 中:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddRazorPages(); services.AddAuthorization(options => { options.AddPolicy("AtLeast18", policy => policy.Requirements.Add(new MinimumAgeRequirement(18))); }); services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); }
上面的例子展示了我們的程序要頒布一條規定(policy), 某些資源需要年滿18歲的 user 才可以訪問. 然后再針對某個 Controller 或者 action, 添加標簽即可:
[Authorize(Policy = "AtLeast18")] public IActionResult GetCangLaoShiVideos() { /// .... return View(); }
在實際的復雜場景中, 可能不僅僅是制定一個策略標簽, 掛載標簽到資源上那么簡單. 需要我們專門制定一些 Authorization Handler, 在 http 消息傳遞給 server 后, 使用這些 handler 來去專門鑒定 user 權限. 所以下面跟隨一個demo 來幫助理解這部分內容.
首先創建一個 requirement 類, 繼承 IAuthorizationRequirement
:
using Microsoft.AspNetCore.Authorization; namespace DennisWu.Account.Service.Requirements { public class MinimumAgeRequirement : IAuthorizationRequirement { public int MinimumAge { get; } public MinimumAgeRequirement(int minimumAge) { MinimumAge = minimumAge; } } }
然后創建 MinimumAgeHandler, 繼承 AuthorizationHandler<MinimumAgeRequirement>
:
using System; using System.Security.Claims; using System.Threading.Tasks; using DennisWu.Account.Service.AuthorizationRequirements; using Microsoft.AspNetCore.Authorization; namespace DennisWu.Account.Service.AuthorizationHandlers { public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeRequirement requirement) { if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://dennisidentityserver.com")) { return Task.CompletedTask; } var dateOfBirth = Convert.ToDateTime( context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://dennisidentityserver.com").Value); int calculatedAge = DateTime.Today.Year - dateOfBirth.Year; if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge)) { calculatedAge--; } if (calculatedAge >= requirement.MinimumAge) { context.Succeed(requirement); } return Task.CompletedTask; } } }
這個 handler 主要目的是驗證 user 的 claims 是否符合要求: 來自受到信任的 issuer: dennisidentityserver.com 並且包含 claim: dateofbirth. 所以如果不符合要求的話, 就會直接進入Task.CompletedTask
返回. 如果符合要求, 並且 age 年滿 18. 會調用 context.Succeed(requirement);