閱讀要求
- 你需要對 netcore 有一些了解
- 有對 JWT 有一定的了解
什么是 驗證 和 授權?
身份驗證(authentication):是確定用戶身份的過程
授權(authorization ):是確定用戶(已經驗證成功的用戶)是否有權訪問資源的過程。
身份驗證
職責:
- 對用戶進行身份驗證。
- 在未經身份驗證的用戶試圖訪問受限資源時作出響應。
現在,我們對一個 action 方法上添加 authorize 特性,這表明我們對這個接口進行了授權:
[HttpGet] [Authorize] public IEnumerable Get(){ return new string[] {"數據1", "數據2"}; }
如果我們直接訪問這個接口,會報如下錯誤:
意思是:你定義了授權,但沒有指定任何(包括自定義和官方的) 身份驗證方案;
授權Authorization 和 認證Authentication 是相輔相成的;兩者缺一不可。
解決的方法,其實報錯信息已經告訴你了;即:添加認證方案的支持,其實,認證方案有很多,但是現在主要推薦的還是 Jwt Bearer 身份驗證方案:
1、Nuget 中安裝 Microsoft.AspNetCore.Authentication.JwtBearer 包;
2、然后再 ConfigurationServices 中添加對 身份驗證的方案(包括使用什么方案,這個方案需要做什么樣子的配置) 做注入容器中處理:
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ixkeE8eu2345k4zs")); // 注意:這里的key不能低於16位 services.AddAuthentication("Bearer") // 注入認證服務,認證類型:Bearer .AddJwtBearer(o => // 注入 Jwt Bearer認證 服務,對其進行配置 { // 對 jwt 進行配置 o.TokenValidationParameters = new TokenValidationParameters() // 對Token的認證是哪些參數,這里設置 { // 這里的參數遵循 3(必要) + 2(可選) 個參數的規則 // 1、是否開啟秘鑰認證,驗證秘鑰 ValidateIssuerSigningKey = true, // 驗證發行者簽名秘鑰 IssuerSigningKey = securityKey, // 發行者簽名秘鑰是? // 2、驗證發行人 ValidateIssuer = true, // 驗證發行者 ValidIssuer = "issuer", // 驗證發行者的名稱是? // 3、驗證訂閱人 ValidateAudience = true, // 是否驗證訂閱者 ValidAudience = "audience", // 驗證訂閱者的名稱是? // 1+1 // 過期時間 和 生命周期 RequireExpirationTime = true, // 使用過期時間 ValidateLifetime = true, // 驗證生命周期 }; });
提醒:根據報錯信息,他有兩種寫法,下面是第二種:
services.AddAuthentication(x => { // 認證方案:Bearer x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // 即:Bearer // 默認Challeng質詢方案: Bearer x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o =>{ // 內容和上面一樣 ... });
配置好后,我們再次訪問接口,發現這回不報錯了,但是返回的狀態碼時 401;
401 Unauthorized:未經授權,身份認證不通過,未認證,可能:無令牌,令牌無效、失效(因為你沒有使用有效的token,無法通過 身份認證 Authentication)
403 Forbidden:被禁止,即:令牌通過,但是你無權限
說明,我們驗證已經起作用了,但因為我們沒有傳遞 jwt token 信息(即沒有權限),對於這個接口的訪問作出了拒絕響應;
接下來,我們新建一個 action 方法,作用是 創建 有效的 token 令牌,然后用這令牌訪問需要授權的api:
// Jwt Token 的生成
[HttpGet] public string GetToekn() { // 注意,必須和上面的 JwtBearer 配置一致,且密鑰最少16位,太少會報錯! SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ixkeE8eu2345k4zs")); // 同樣,我們在上面的 JwtBearer 配置中,需要驗證的是什么,這里也要生成對應的條件,缺了就會導致認證失敗,假如這里發行人改成其他,驗證那邊就不通過 SecurityToken securityToken = new JwtSecurityToken( // 和上面一樣,同樣遵循 3+2 規則 issuer: "issuer", // 發行人 audience: "audience", // 訂閱人 signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256), // 安全密鑰 和 加密算法 expires: DateTime.Now.AddHours(1), // 過期時間 claims: new Claim[] { } // 添加 Claim(聲明主體),添加uid、username、role等都放在這里 ); return new JwtSecurityTokenHandler().WriteToken(securityToken); // 返回 Token字符串 }
最后,我們還需要添加 身份驗證 UseAuthentication 中間件,授權 Authorization 中間件,要不然http請求管道中沒有處理 身份認證 了:
app.UseHttpsRedirection(); // 注意下面 Routing Authentication Authorization 這三個中間件的放置順序,必須按照這個順序: app.UseRouting(); // 添加 身份驗證 中間件(注意順序,中間件這里是:先身份驗證再授權)、 // 而且 身份驗證 和 授權 都要放在Routing 之后 app.UseAuthentication(); // 添加 身份認證中間件 // 添加 授權中間件 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
運行項目,先獲取 token:
使用 這個token訪問需要授權的api:
或者你也可以這么訪問:
至此,驗證 和 授權 已經講完了,你需要好好思考下上面的流程;
上面內容中,大多介紹了身份驗證,如何使用,而接下去,我們將着重介紹 授權;
授權
上面例子中,我么用 authorize 這個特性作用於一個 action 方法上了,這就是授權,對這個action進行了權限限制;
但是,這種簡單的授權,是只要有效的token(即:身份驗證通過),就能訪問這個接口,而沒有精細化處理(就好比:董事長有這權限、行政也有着權限、而員工沒有這權限);
asp.net core 的授權分三種
1、普通的授權(上面已經講了)
2、基於角色的授權
3、基於策略的授權
基於角色的授權:
[HttpGet] [Authorize(Roles="Vip")] public string Get(){ ... }
之前,我們在編寫獲取 Jwt Token 時,定義的 Claim 是空數組,現在,我們加點東西下去:
... expires: DateTime.Now.AddHours(1), claims: new Claim[] { new Claim("kuozhan", "kuozhanneirong"), // 可以用我們自己定義的 name new Claim(ClaimTypes.Role, "Vip"), // 也可以用內置的name,如 Role,這里就是我們的授權,給誰 new Claim(ClaimTypes.Email, "123@qq.com") } // 添加 Claim(聲明主體),添加uid、username、role等都放在這里 ); ...
ClaimTyps.Role的參數對應的就是 action 上面特性的Role參數;
我們通過這種方式生成的 Token 就可以訪問對 Roles=Vip 的action的權限訪問了;
基於策略
假如,一個action不止 Vip 這個權限,還有五六個呢?如何把他們合並在一起?這就使用到了 基於策略的 授權機制了:
[HttpGet] [Authorize(Policy="AdminAndUser")] public IEnumerate Get(){ return new string[] {"數據1", "數據2"};l }
然后我們注入服務:
// 策略注入服務: services.AddAuthorization( o => { o.AddPolicy("AdminOrUser", o=>{ o.RequireRole("Admin", "User").Build(); }); });
這樣,A用戶有Admin、B用戶有User,它們兩都能訪問整個接口;
如果要求,用戶必須有Admin 和 User 才能訪問,那么可以:
// 策略注入服務: services.AddAuthorization( o => { o.AddPolicy("AdminOrUser", o=>{ o.RequireRole("Admin").RequireRole("User").Build(); }); });
自定義策略的授權
// 新建一個類: /PolicyRequirement/MustRoleAdminHandle.cs using Microsoft.AspNetCore.Authorization; using System.Linq; using System.Threading.Tasks;
// 自定義策略授權,繼承至 IAuthorizationHandler 且實現里面的 HandleAsync 方法 即可 public class MustRoleAdminHandle : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { // 做驗證判斷,如果驗證通過,則 context.Successed(); // 可以看形參 context 里的各種屬性 // 比如: //var requirement = context.Requirements.FirstOrDefault(); //context.Succeed(requirement); // 這樣設置后就返回 200 // 或者設置 Fail() // context.Fail(); return Task.CompletedTask; // 直接這么返回,會返回 403 } }
// 服務注入(注意這個): using Microsoft.AspNetCore.Authorization; services.AddSingleton<IAuthorizationHandler, MustRoleAdminHandle>();
思考:context.Requirements 里面的值是什么? 是對應 api接口上策略名里面的所有Role集合,即:
action上定義的是 UserAndAdmin時,其策略注冊是 o.RequireRole("Admin", "User") 那么,集合就是: Admin和User;
基於 Claim聲明 的授權
services.AddAuthorization(o=>{ o.AddPolicy("AdminClaim2", 0 => { o.RequireClaim("Email", "123@qq.com", "456@qq.com"); // JWT Token里面的 Claim 中設置了這些,那么就會通過 AdminClaim2 }); });
基於Requirement需要,大多數開發都是用這種方式:
[HttpGet] [Authorize(Policy="AdminRequireMent")] public string Get(){ return string.Empty();} // 服務注入 services.AddAuthorization(o=>{ o.AddPolicy("AdminRequireMent", o => { var myAdminRequirement = new AdminRequirement(myName = "zhangsanfeng"); // 可以傳遞參數 o.Requirements.Add(myAdminRequirement); // 完全自定義 }); }); // 需要新建一個 /PolicyRequirement/AdminRequirement.cs 並繼承至 IAuthorizationRequirement using using Microsoft.AspNetCore.Authorization; public class AdminRequirement: IAuthorizationRequirement{ public string myName { get; set; }; }
這樣,我們重啟再次訪問有授權的Get接口時,會先進入這里:
public class MustRoleAdminHandle : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { context.Requirements // 該參數返回的就是 myAdminRequirement 實例 // 這樣,我們就可以通過這個自定義判斷 // 非常的靈活 } }
但是,上面代碼中,IAuthorizationHandler 接口並不是很靈活,微軟又抽象了一個 抽象類 AuthorizationHandler ,這樣,我們就更加容易去使用了:
public class MustRoleAdminHandle:AuthorizationHandler<AdminRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement) { // context.Succeed(requirement); return Task.CompletedTask; // 直接這么返回,會返回 403 } }
JwtBearer認證中,默認是通過http的Authorization頭來獲取的,也是推薦這種做法,但是某些場景下需要通過url或者cookie中來傳遞Token,如何實現呢?
從url中獲取的,可以:
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("abcdefghijklnmopq")); // 為key不能低於16位 services.AddAuthentication("Bearer") // 注入認證服務,認證類型:Bearer .AddJwtBearer(o => // 注入 Jwt Bearer認證 服務,對其進行配置 { // 主要是這里: o.Events = new JwtBearerEvents(){ OnMessageREceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; } }; o.TokenValidationParameters = new TokenValidationParameters{ //... } });
除了OnMessageReceived外,還提供了如下幾個事件:
- TokenValidated:在Token驗證通過后調用。
- AuthenticationFailed: 認證失敗時調用。
- Challenge: 未授權時調用。
使用OIDC服務(即:OpenId Connect,身份認證(核心部分))
簡單來說:OIDC是OpenID Connect的簡稱,OIDC=(Identity, Authentication) + OAuth 2.0。它在OAuth2上構建了一個身份層,是一個基於OAuth2協議的身份認證標准協議。
在上面的示例中,我們簡單模擬的Token頒發,功能非常簡單,但是這並不適合在生產環境中使用,可是微軟也沒有提供OIDC服務的實現,好在.NET社區中提供了幾種實現,可供我們選擇:
AspNet.Security.OpenIdConnect.Server (ASOS)、IdentityServer4、OpenIddict 和 PwdLess
我們在這里使用IdentityServer4來搭建一個OIDC服務器,具體代碼會給大家帶來混淆,所以這里忽略了。
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "xxxProjectNet5.Api", Version = "v1" }); // =======================================================按照下面的格式即可 // 開啟小鎖 c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); // 在 header 中添加token,傳遞到后台 c.OperationFilter<SecurityRequirementsOperationFilter>(); c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme() { Description = "JWT 授權(數據將在請求頭中進行傳輸)直接在下框中輸入 Bearer(token)(注意兩者之間是一個空格)", Name = "Authorization", // jwt 默認的參數名稱 In = ParameterLocation.Header, // jwt 將默認存放 Authorization 信息的位置(請求頭中) Type = SecuritySchemeType.ApiKey // 除了ApiKey 外,還有 Http、Oauth2、OpenIdConnect }); });
如果你有任何問題,歡迎留下評論,我們一起探討