序言
本文將分別介紹 Authentication(認證) 和 Authorization(授權)。
并以簡單的例子在 ASP.NET Core 6.0 的 WebAPI 中分別實現這兩個功能。
相關名詞
Authentication 和 Authorization 長得很像,傻傻分不清楚。
Authentication(認證):標識用戶的身份,一般發生在登錄的時候。
Authorization(授權):授予用戶權限,指定用戶能訪問哪些資源;授權的前提是知道這個用戶是誰,所以授權必須在認證之后。
認證(Authentication)
基本步驟
- 安裝相關 Nuget 包:Microsoft.AspNetCore.Authentication.JwtBearer
- 準備配置信息(密鑰等)
- 添加服務
- 調用中間件
- 實現一個 JwtHelper,用于生成 Token
- 控制器限制訪問(添加 Authorize 標簽)
1 安裝 Nuget 包
安裝 Microsoft.AspNetCore.Authentication.JwtBearer
在程序包管理器控制臺中:
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.1
2 準備配置信息
在 Appsetting.json 中,添加一個 Jwt 節點
"Jwt": {
"SecretKey": "lisheng741@qq.com",
"Issuer": "WebAppIssuer",
"Audience": "WebAppAudience"
}
3 添加服務
在 Program.cs 文件中注冊服務。
// 引入所需的命名空間
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// ……
var configuration = builder.Configuration;
// 注冊服務
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true, //是否驗證Issuer
ValidIssuer = configuration["Jwt:Issuer"], //發行人Issuer
ValidateAudience = true, //是否驗證Audience
ValidAudience = configuration["Jwt:Audience"], //訂閱人Audience
ValidateIssuerSigningKey = true, //是否驗證SecurityKey
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])), //SecurityKey
ValidateLifetime = true, //是否驗證失效時間
ClockSkew = TimeSpan.FromSeconds(30), //過期時間容錯值,解決服務器端時間不同步問題(秒)
RequireExpirationTime = true,
};
});
4 調用中間件
調用 UseAuthentication(認證),必須在所有需要身份認證的中間件前調用,比如 UseAuthorization(授權)。
// ……
app.UseAuthentication();
app.UseAuthorization();
// ……
5 JwtHelper 類實現
主要是用于生成 JWT 的 Token。
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace TestWebApi;
public class JwtHelper
{
private readonly IConfiguration _configuration;
public JwtHelper(IConfiguration configuration)
{
_configuration = configuration;
}
public string CreateToken()
{
// 1. 定義需要使用到的Claims
var claims = new[]
{
new Claim(ClaimTypes.Name, "u_admin"), //HttpContext.User.Identity.Name
new Claim(ClaimTypes.Role, "r_admin"), //HttpContext.User.IsInRole("r_admin")
new Claim(JwtRegisteredClaimNames.Jti, "admin"),
new Claim("Username", "Admin"),
new Claim("Name", "超級管理員")
};
// 2. 從 appsettings.json 中讀取SecretKey
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));
// 3. 選擇加密算法
var algorithm = SecurityAlgorithms.HmacSha256;
// 4. 生成Credentials
var signingCredentials = new SigningCredentials(secretKey, algorithm);
// 5. 根據以上,生成token
var jwtSecurityToken = new JwtSecurityToken(
_configuration["Jwt:Issuer"], //Issuer
_configuration["Jwt:Audience"], //Audience
claims, //Claims,
DateTime.Now, //notBefore
DateTime.Now.AddSeconds(30), //expires
signingCredentials //Credentials
);
// 6. 將token變為string
var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return token;
}
}
該 JwtHelper 依賴于 IConfiguration(為了讀取配置文件),將 JwtHelper 的創建交由 DI 容器,在 Program.cs 中添加服務:
var configuration = builder.Configuration;
builder.Services.AddSingleton(new JwtHelper(configuration));
將 JwtHelper 注冊為單例模式。
6 控制器配置
新建一個 AccountController,以構造函數方式注入 JwtHelper,添加兩個 Action:GetToken 用于獲取 Token,GetTest 打上 [Authorize] 標簽用于驗證認證。
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace TestWebApi.Controllers;
[Route("api/[controller]/[action]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly JwtHelper _jwtHelper;
public AccountController(JwtHelper jwtHelper)
{
_jwtHelper = jwtHelper;
}
[HttpGet]
public ActionResult<string> GetToken()
{
return _jwtHelper.CreateToken();
}
[Authorize]
[HttpGet]
public ActionResult<string> GetTest()
{
return "Test Authorize";
}
}
7 測試調用
方式一:通過 Postman、Apifox 等接口調試軟件調試
使用 Postman 調用 /api/Account/GetToken 生成 Token
在調用 /api/Account/GetTest 時傳入 Token,得到返回結果
方式二:在瀏覽器控制臺調試
調試 /api/Account/GetToken
var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
if(this.readyState === 4) {
console.log(token = this.responseText); //這里用了一個全局變量 token,為下一個接口服務
}
});
xhr.open("GET", "/api/Account/GetToken");
xhr.send();
調試 /api/Account/GetTest
var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
if(this.readyState === 4) {
console.log(this.status, this.responseText); //this.status為響應狀態碼,401為無認證狀態
}
});
xhr.open("GET", "/api/Account/GetTest");
xhr.setRequestHeader("Authorization",`Bearer ${token}`); //附帶上 token
xhr.send();
授權(Authorization)
注意:授權必須基于認證,即:若沒有完成上文關于認證的配置,則下面的授權是不會成功的。
授權部分,將先介紹相關標簽、授權方式,再介紹基于策略的授權。這三部分大致的內容如下描述:
相關標簽:Authorize 和 AllowAnonymous
授權方式:介紹 Policy、Role、Scheme 的基本內容
基于策略(Policy)的授權:深入 Policy 授權方式
相關標簽(Attribute)
授權相關標簽具體請查考官方文檔簡單授權
[Authorize]
打上該標簽的 Controller 或 Action 必須經過認證,且可以標識需要滿足哪些授權規則。
授權規則可以是 Policy(策略)、Roles(角色) 或 AuthenticationSchemes(方案)。
[Authorize(Policy = "", Roles ="", AuthenticationSchemes ="")]
[AllowAnonymous]
允許匿名訪問,級別高于 [Authorize] ,若兩者同時作用,將生效 [AllowAnonymous]
授權方式
基本上授權只有:Policy、Role、Scheme 這3種方式,對應 Authorize 標簽的3個屬性。
1 Policy(策略)
推薦的授權方式,在 ASP.NET Core 的官方文檔提及最多的。一個 Policy 可以包含多個要求(要求可能是 Role 匹配,也可能是 Claims 匹配,也可能是其他方式。)
下面舉個基礎例子(說是基礎例子,主要是基于 Policy 的授權方式可以不斷深入追加一些配置):
在 Program.cs 中,添加兩條 Policy:
policy1 要求用戶擁有一個 Claim,其 ClaimType 值為 EmployeeNumber。
policy2 要求用戶擁有一個 Claim,其 ClaimType 值為 EmployeeNumber,且其 ClaimValue 值為1、2、3、4 或 5。
builder.Services.AddAuthorization(options => {
options.AddPolicy("policy1", policy => policy.RequireClaim("EmployeeNumber"));
options.AddPolicy("policy2", policy => policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
})
在控制器中添加 [Authorize] 標簽即可生效:
[Authorize(Policy = "policy1")]
public class TestController : ControllerBase
或在控制器的 Action 上:
public class TestController : ControllerBase
{
[Authorize(Policy = "policy1")]
public ActionResult<string> GetTest => "GetTest";
}
2 Role(角色)
基于角色授權,只要用戶擁有角色,即可通過授權驗證。
在認證時,給用戶添加角色相關的 Claim ,即可標識用戶擁有的角色(注:一個用戶可以擁有多個角色的 Claim),如:
new Claim(ClaimTypes.Role, "admin"),
new Claim(ClaimTypes.Role, "user")
在 Controller 或 Action 中:
[Authorize(Roles = "user")]
public class TestController : ControllerBase
{
public ActionResult<string> GetUser => "GetUser";
[Authorize(Roles = "admin")] //與控制器的Authorize疊加作用,除了擁有user,還需擁有admin
public ActionResult<string> GetAdmin => "GetAdmin";
[Authorize(Roles = "user,admin")] //user 或 admin 其一滿足即可
public ActionResult<string> GetUserOrAdmin => "GetUserOrAdmin";
}
3 Scheme(方案)
方案如:Cookies 和 Bearer,當然也可以是自定義的方案。
由于這種方式不常用,這里不做展開,請參考官方文檔按方案限制標識。
基于策略(Policy)的授權
上面已經提及了一個基于策略授權的基礎例子,下面將繼續深入這種授權方式。
1 授權過程
在不斷深入 Policy 這種方式的授權之前,有必要將授權的過程描述一下。授權過程描述建議結合源碼查看,這樣能更清楚其中的作用。當然,這一部分是比較難懂,筆者表述可能也不夠清晰,而這一部分對于完成授權的配置也不會有影響,故而如果讀者看不明白或無法理解,可以暫且跳過,不必糾結。
建議看一下ASP.NET Core 認證與授權6:授權策略是怎么執行的?這篇文章,文章將授權相關的源碼整理出來了,并說明了其中的關系。
這里簡單梳理一下:
與授權相關的 interface 和 class 如下:
IAuthorizationService #驗證授權的服務,主要方法AuthorizeAsync
DefaultAuthorizationService #IAuthorizationService的默認實現
IAuthorizationHandler #負責檢查是否滿足要求,主要方法HandleAsync
IAuthorizationRequirement #只有屬性,沒有方法;用于標記服務,以及用于追蹤授權是否成功的機制。
AuthorizationHandler<TRequirement> #主要方法HandleRequirementAsync
這些 interface 和 class 的關系以及授權過程是這樣的:
DefaultAuthorizationService 實現 IAuthorizationService 的 AuthorizeAsync 方法。
AuthorizeAsync 方法會獲取到所有實現了 IAuthorizationHandler 的實例,并循環調用所有實例的 HandleAsync 方法檢查是否滿足授權要求,如果有任一一個 HandleAsync 返回了 Fail 則將結束循環(細節請參考官方文檔處理程序返回結果),并禁止用戶訪問。
IAuthorizationHandler 的作用如上一點所述,提供了一個 HandleAsync 方法,用于檢查授權。
IAuthorizationRequirement 是一個要求,主要是配合 AuthorizationHandler<TRequirement> 使用。
AuthorizationHandler<TRequirement> 實現了 IAuthorizationHandler 的 HandleAsync 方法,并提供了一個 HandleRequirementAsync 的方法。HandleRequirementAsync 用于檢查 Requirement(要求)是否滿足。HandleAsync 的默認實現為獲取所有實現 TRequirement 的請求(且該請求由 Policy 添加進列表里),循環調用 HandleRequirementAsync,檢查哪個要求(Requirement)能滿足授權。
簡述一下:
[Authorize] 標簽生效時,調用的是 IAuthorizationService 的 AuthorizeAsync(由 DefaultAuthorizationService 實現)。
AuthorizeAsync 會去調用所有 IAuthorizationHandler 的 HandleAsync (由 AuthorizationHandler<TRequirement> 實現)。
HandleAsync 會去調用 AuthorizationHandler<TRequirement> 的HandleRequirementAsync 的方法。
注意:這里只是列出了主要的接口和類,部分沒有列出,如:IAuthorizationHandlerProvider(這個接口的默認實現 DefaultAuthorizationHandlerProvider,主要是用于收集 IAuthorizationHandler 并返回 IEnumerable<IAuthorizationHandler>)
2 實現說明
IAuthorizationService 已默認實現,不需要我們做額外工作。
IAuthorizationHandler 由 AuthorizationHandler<TRequirement> 實現。
所以我們要做的,是:
第一步,準備 Requirement 實現 IAuthorizationRequirement
第二步,添加一個 Handler 程序繼承 AuthorizationHandler<TRequirement> 并重寫 HandleRequirementAsync 方法
關于具體實現,可以參考ASP.NET Core 認證與授權7:動態授權基于權限的授權部分,該文章思路已十分清晰,這里將其主要步驟列出來。
3 定義權限項
在實現 Requirement 之前,我們需要先定義一些權限項,主要用于后續作為 Policy 的名稱,并傳入 我們實現的 Requirement 之中。
public static class UserPermission
{
public const string User = "User";
public const string UserCreate = User + ".Create";
public const string UserDelete = User + ".Delete";
public const string UserUpdate = User + ".Update";
}
如上,定義了“增”、“刪”、“改”等權限,其中 User 將擁有完整權限。
4 實現 Requirement
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string name)
{
Name = name;
}
public string Name { get; set; }
}
使用 Name 屬性表示權限的名稱,與 UserPermission 中的常量對應。
5 實現授權處理程序 Handler
這里假定用戶的 Claim 中 ClaimType 為 Permission 的項,如:
new Claim("Permission", UserPermission.UserCreate),
new Claim("Permission", UserPermission.UserUpdate)
如上,標識該用戶用戶 UserCreate 和 UserUpdate 的權限。
注意:當然,實際程序我們肯定不是這樣實現的,這里只是簡易示例。
接著,實現一個授權 Handler:
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
var permissions = context.User.Claims.Where(_ => _.Type == "Permission").Select(_ => _.Value).ToList();
if (permissions.Any(_ => _.StartsWith(requirement.Name)))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
運行 HandleRequirementAsync 時,會將用戶的 Claim 中 ClaimType 為 Permission 的項取出,并獲取其 Value 組成一個 List<string>。
接著驗證 Requirement 是否滿足授權,滿足則運行 context.Succeed 。
6 添加授權處理程序
在 Program.cs 中,將 PermissionAuthorizationHandler 添加到 DI 中:
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
7 添加授權策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(UserPermission.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserCreate)));
options.AddPolicy(UserPermission.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserUpdate)));
options.AddPolicy(UserPermission.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserDelete)));
});
8 控制器配置
控制器如下:
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
[HttpGet]
[Authorize(UserPermission.UserCreate)]
public ActionResult<string> UserCreate() => "UserCreate";
[HttpGet]
[Authorize(UserPermission.UserUpdate)]
public ActionResult<string> UserUpdate() => "UserUpdate";
[HttpGet]
[Authorize(UserPermission.UserDelete)]
public ActionResult<string> UserDelete() => "UserDelete";
}
基于上面的假定,用戶訪問接口的情況如下:
/api/User/UserCreate #成功
/api/User/UserUpdate #成功
/api/User/UserDelete #403無權限
至此,基于策略(Policy)的授權其實已經基本完成。
接下去的內容,將是對上面一些內容的完善或補充。
完善:實現策略提供程序 PolicyProvider
一般添加授權策略如下是在 Program.cs 中,方式如下:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("policy", policy => policy.RequireClaim("EmployeeNumber"));
});
通過 AuthorizationOptions.AddPolicy 添加授權策略這種方式不靈活,無法動態添加。
通過實現 IAuthorizationPolicyProvider 并添加到 DI 中,可以實現動態添加 Policy。
IAuthorizationPolicyProvider 的默認實現為 DefaultAuthorizationPolicyProvider 。
實現一個 PolicyProvider 如下:
public class TestAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider, IAuthorizationPolicyProvider
{
public Test2AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) {}
public new Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> return base.GetDefaultPolicyAsync();
public new Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
return base.GetFallbackPolicyAsync();
public new Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(UserPermission.User))
{
var policy = new AuthorizationPolicyBuilder("Bearer");
policy.AddRequirements(new PermissionAuthorizationRequirement(policyName));
return Task.FromResult<AuthorizationPolicy?>(policy.Build());
}
return base.GetPolicyAsync(policyName);
}
}
注意:自定義的 TestAuthorizationPolicyProvider 必須實現 IAuthorizationPolicyProvider,否則添加到 DI 時會不生效。
在 Program.cs 中,將自定義的 PolicyProvider 添加到 DI 中:
builder.Services.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();
注意:只會生效最后一個添加的 PolicyProvider。
補充:自定義 AuthorizationMiddleware
自定義 AuthorizationMiddleware 可以:
- 返回自定義的響應
- 增強(或者說改變)默認的 challenge 或 forbid 響應
具體請查看官方文檔自定義 AuthorizationMiddleware 的行為
補充:MiniApi 的授權
在 MiniApi 中幾乎都是形如 MapGet() 的分支節點,這類終結點無法使用 [Authorize] 標簽,可以用使用 RequireAuthorization("Something") 進行授權,如:
app.MapGet("/helloworld", () => "Hello World!")
.RequireAuthorization("AtLeast21");
作者:蘆薈柚子茶
鏈接:
https://www.cnblogs.com/clis/p/16151872.html