10分鐘簡單學習net core集成jwt許可權認證,快速接入項目落地使用
什麼是JWT
JSON Web Token(JWT)是目前最流行的跨域身份驗證、分散式登錄、單點登錄等解決方案。
JWT的官網地址://jwt.io/
通俗地來講,JWT是能代表用戶身份的令牌,可以使用JWT令牌在api介面中校驗用戶的身份以確認用戶是否有訪問api的許可權。
JWT中包含了身份認證必須的參數以及用戶自定義的參數,JWT可以使用秘密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
JSON Web令牌能做什麼?
- 授權:這是使用JWT的最常見方案。一旦用戶登錄,每個後續請求將包括JWT,允許用戶訪問該令牌允許的路由,服務和資源。Single Sign On是一種現在廣泛使用JWT的功能,因為它的開銷很小,並且能夠在不同的域中輕鬆使用。
- 資訊交換:JSON Web令牌是在各方之間安全傳輸資訊的好方法。因為JWT可以簽名 – 例如,使用公鑰/私鑰對 – 您可以確定發件人是他們所說的人。此外,由於使用標頭和有效負載計算簽名,您還可以驗證內容是否未被篡改。
JSON Web令牌如何工作?
在身份驗證中,當用戶使用其憑據成功登錄時,將返回JSON Web令牌。由於令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,您不應該將令牌保留的時間超過要求。
每當用戶想要訪問受保護的路由或資源時,用戶代理應該使用承載模式發送JWT,通常在Authorization標頭中,標題的內容應如下所示:
Authorization: Bearer <token>
在某些情況下,這可以是無狀態授權機制。伺服器的受保護路由將檢查Authorization
標頭中的有效JWT ,如果存在,則允許用戶訪問受保護資源。如果JWT包含必要的數據,則可以減少查詢資料庫以進行某些操作的需要,儘管可能並非總是如此。
如果在標Authorization
頭中發送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。
下圖顯示了如何獲取JWT並用於訪問API或資源:
1、應用程式向授權伺服器請求授權;
2、校驗用戶身份,校驗成功,返回token;
3、應用程式使用訪問令牌訪問受保護的資源。
JWT的實現方式是將用戶資訊存儲在客戶端,服務端不進行保存。每次請求都把令牌帶上以校驗用戶登錄狀態,這樣服務就變成了無狀態的,伺服器集群也很好擴展。
更多理論知識可以查看官網,或者查看相關網友的文章,如下推薦文章:
- asp.net core 集成JWT(一)://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
- 五分鐘帶你了解啥是JWT://zhuanlan.zhihu.com/p/86937325
- C#分散式登錄——jwt://www.cnblogs.com/yswenli/p/13510050.html
net core 集成jwt程式碼實現
新建項目
首先我們新建一個ASP.NET Core Web API項目,命名為 jwtWebAPI,選擇目標框架.NET Core3.1,注意,如果勾選了https配置,postman請求的時候要設置去除ssl認證才能使用,建議不配置https。
在nuget裡面引用jwt集成的程式包,這裡需要注意的是,如果你用的是.NET Core 3.1的框架的話,程式包版本選擇3.1.10
Microsoft.AspNetCore.Authentication.JwtBearer
添加數據訪問模擬api,新建控制器ValuesController
其中api/value1是可以直接訪問的,api/value2添加了許可權校驗特性標籤 [Authorize]
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace jwtWebAPI.Controllers { [ApiController] public class ValuesController : ControllerBase { [HttpGet] [Route("api/values1")] public ActionResult<IEnumerable<string>> values1() { return new string[] { "value1", "value1" }; } /** * 該介面用Authorize特性做了許可權校驗,如果沒有通過許可權校驗,則http返回狀態碼為401 * 調用該介面的正確姿勢是: * 1.登陸,調用api/Auth介面獲取到token * 2.調用該介面 api/value2 在請求的Header中添加參數 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzM1MzM3IiwiZXhwIjoxNTYwMzM3MTM3LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.1S-40SrA4po2l4lB_QdzON_G5ZNT4P_6U25xhTcl7hI * Bearer後面有空格,且後面是第一步中介面返回的token值 * */ [HttpGet] [Route("api/value2")] [Authorize] public ActionResult<IEnumerable<string>> value2() { //這是獲取自定義參數的方法 var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims; var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value; return new string[] { "訪問成功:這個介面登陸過的用戶都可以訪問", $"userName={userName}" }; } } }
添加模擬登陸生成Token的api,新建控制器AuthController
這裡模擬一下登陸校驗,只驗證了用戶密碼不為空即通過校驗,真實環境完善校驗用戶和密碼的邏輯。
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; namespace jwtWebAPI.Controllers { [ApiController] public class AuthController : Controller { /// <summary> /// 通過帳號+密碼獲取Token /// </summary> /// <param name="userName"></param> /// <param name="pwd"></param> /// <returns>Token</returns> [AllowAnonymous] [HttpGet] [Route("api/auth")] public IActionResult GetToken(string userName, string pwd) { if (!string.IsNullOrEmpty(userName)) { //每次登陸動態刷新 Const.ValidAudience = userName + pwd + DateTime.Now.ToString(); // push the user』s name into a claim, so we can identify the user later on. //這裡可以隨意加入自定義的參數,key可以自己隨便起 var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(3)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.NameIdentifier, userName) }; //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //.NET Core』s JwtSecurityToken class takes on the heavy lifting and actually creates the token. var token = new JwtSecurityToken( //頒發者 issuer: Const.Domain, //接收者 audience: Const.ValidAudience, //過期時間(可自行設定,注意和上面的claims內部Exp參數保持一致) expires: DateTime.Now.AddMinutes(3), //簽名證書 signingCredentials: creds, //自定義參數 claims: claims ); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } } } }
Startup添加JWT驗證的相關配置
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace jwtWebAPI { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //添加jwt驗證: services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = true,//是否驗證失效時間 ClockSkew = TimeSpan.FromSeconds(30), //時間偏移量(允許誤差時間) ValidateAudience = true,//是否驗證Audience(驗證之前的token是否失效) //ValidAudience = Const.GetValidudience(),//Audience //這裡採用動態驗證的方式,在重新登陸時,刷新token,舊token就強制失效了 AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//是否驗證Issuer(頒發者) ValidAudience = Const.Domain,//Audience 【Const是新建的一個常量類】 接收者 ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設置一致 頒發者 ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘鑰SecurityKey }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { //Token expired if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { //添加jwt驗證 app.UseAuthentication(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
創建常量類Const
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace jwtWebAPI { public class Const { /// <summary> /// 這裡為了演示,寫死一個密鑰。實際生產環境可以從配置文件讀取,這個是用網上工具隨便生成的一個密鑰(md5或者其他都可以) /// </summary> public const string SecurityKey = "48754F4C58F9EA428FE09D714E468211"; /// <summary> /// 站點地址(頒發者、接受者),這裡測試和當前本地運行網站相同,實際發到正式環境應為域名地址 /// </summary> public const string Domain = "//localhost:44345"; /// <summary> /// 受理人,之所以弄成可變的是為了用介面動態更改這個值以模擬強制Token失效 /// 真實業務場景可以在資料庫或者redis存一個和用戶id相關的值,生成token和驗證token的時候獲取到持久化的值去校驗 /// 如果重新登陸,則刷新這個值 /// </summary> public static string ValidAudience; } }
JWT登錄授權測試成功
把程式編譯運行起來,打開postman,輸入地址,首先測試不需要任何授權的
正確地返回了數據,那麼接下來我們測試JWT的流程。
首先我們什麼都不加調用介面://localhost:44345/api/values2,注意,我創建的時候是https的,大家注意看是http還是https
返回了狀態碼401,也就是未經授權:訪問由於憑據無效被拒絕。 說明JWT校驗生效了,我們的介面收到了保護。
調用模擬登陸授權介面://localhost:44345/api/auth?userName=xiongze&pwd=123456
這裡的用戶密碼是隨便寫的,因為我們模擬登陸只是校驗了下非空,因此寫什麼都能通過。
然後我們得到了一個xxx.yyy.zzz 格式的 token 值。我們把token複製出來。
在剛才401的介面(//localhost:44345/api/values2)請求header中添加JWT的參數,把我們的token加上去
再次調用我們的模擬數據介面,但是這次我們加了一個header,KEY:Authorization Value:Bearer Tokne的值
這裡需要注意 Bearer 後面是有一個空格的,然後就是我們上一步獲取到的token,
得到返回值,正確授權成功,我們是支援自定義返回參數的,上面程式碼裡面有相關內容,比如用戶名這些不敏感的資訊可以帶著返回。
等token設置的過期時間到了,或者重新生成了新的Token,沒有及時更新,那麼我們的授權也到期,401,
升級操作:介面許可權隔離
上面的操作是所有登錄授權成功的角色都可以進行調用所有介面,那麼我們現在想要進行介面隔離限制,
也就是說,雖然授權登錄了,但是我這個介面是指定許可權訪問的。
比如說:刪除介面只能管理員角色操作,那麼其他角色雖然授權登錄了,但是沒有許可權調用刪除介面。
我們在原來的操作進行改造升級看一下。
添加類
新建一個AuthManagement文件夾,添加PolicyRequirement類和PolicyHandler類,
PolicyRequirement類:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace jwtWebAPI.AuthManagement { /// <summary> /// 許可權承載實體 /// </summary> public class PolicyRequirement : IAuthorizationRequirement { /// <summary> /// 用戶許可權集合 /// </summary> public List<UserPermission> UserPermissions { get; private set; } /// <summary> /// 無許可權action /// </summary> public string DeniedAction { get; set; } /// <summary> /// 構造 /// </summary> public PolicyRequirement() { //沒有許可權則跳轉到這個路由 DeniedAction = new PathString("/api/nopermission"); //用戶有許可權訪問的路由配置,當然可以從資料庫獲取 UserPermissions = new List<UserPermission> { new UserPermission { Url="/api/values3", UserName="admin"}, }; } } /// <summary> /// 用戶許可權承載實體 /// </summary> public class UserPermission { /// <summary> /// 用戶名 /// </summary> public string UserName { get; set; } /// <summary> /// 請求Url /// </summary> public string Url { get; set; } } }
PolicyHandler類(注意2.x和3.x的區別)
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace jwtWebAPI.AuthManagement { public class PolicyHandler : AuthorizationHandler<PolicyRequirement> { private readonly IHttpContextAccessor _httpContextAccessor; public PolicyHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) { //賦值用戶許可權 var userPermissions = requirement.UserPermissions; var httpContext = _httpContextAccessor.HttpContext; //請求Url var questUrl = httpContext.Request.Path.Value.ToUpperInvariant(); //是否經過驗證 var isAuthenticated = httpContext.User.Identity.IsAuthenticated; if (isAuthenticated) { if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl)) { //用戶名 var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value; if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl)) { context.Succeed(requirement); } else { ////無許可權跳轉到拒絕頁面 //httpContext.Response.Redirect(requirement.DeniedAction); return Task.CompletedTask; } } else { context.Succeed(requirement); } } return Task.CompletedTask; } } }
添加指定角色
在 AuthController 控制器的GetToken授權加入自定義的參數,如下
new Claim(“Role”, userName) //這裡是角色,我使用登錄帳號admin代替
在 AuthController 控制器裡面添加無許可權訪問的方法
[AllowAnonymous] [HttpGet] [Route("api/nopermission")] public IActionResult NoPermission() { return Forbid("No Permission!"); }
修改Startup配置
在startup.cs的ConfigureServices 方法裡面添加策略鑒權模式、添加JWT Scheme、注入授權Handler
修改後的文件如下
using jwtWebAPI.AuthManagement; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace jwtWebAPI { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services //添加策略鑒權模式 .AddAuthorization(options => { options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement())); }) //添加JWT Scheme .AddAuthentication(s => { s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) //添加jwt驗證: .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = true,//是否驗證失效時間 ClockSkew = TimeSpan.FromSeconds(30), //時間偏移量(允許誤差時間) ValidateAudience = true,//是否驗證Audience(驗證之前的token是否失效) //ValidAudience = Const.GetValidudience(),//Audience //這裡採用動態驗證的方式,在重新登陸時,刷新token,舊token就強制失效了 AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//是否驗證Issuer(頒發者) ValidAudience = Const.Domain,//Audience 【Const是新建的一個常量類】 接收者 ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設置一致 頒發者 ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘鑰SecurityKey }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { //Token expired if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); //注入授權Handler services.AddSingleton<IAuthorizationHandler, PolicyHandler>(); //注入獲取HttpContext services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { //添加jwt驗證 app.UseAuthentication(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
添加api訪問的方法
在 ValuesController控制器添加指定許可權訪問的方法,如下:
/** * 這個介面必須用admin **/ [HttpGet] [Route("api/values3")] [Authorize("Permission")] public ActionResult<IEnumerable<string>> values3() { //這是獲取自定義參數的方法 var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims; var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value; var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value; return new string[] { "訪問成功:這個介面有管理員許可權才可以訪問", $"userName={userName}", $"Role={role}" }; }
不同許可權測試訪問
我們同樣的方法去模擬登錄,//localhost:44345/api/auth?userName=xiongze&pwd=123
注意,帳號先不用admin登錄,然後用返回的token去請求我們剛剛添加的指定許可權訪問的介面,這個時候是沒有許可權訪問的,因為這個是admin許可權訪問。
我們同樣的方法去模擬登錄,//localhost:44345/api/auth?userName=admin&pwd=123
訪問成功。
源碼下載地址
Gitee://gitee.com/xiongze/jwtWebAPI.git
參考文獻
- asp.net core 集成JWT(一)://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
- 五分鐘帶你了解啥是JWT://zhuanlan.zhihu.com/p/86937325
- C#分散式登錄——jwt://www.cnblogs.com/yswenli/p/13510050.html
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂 公眾號:熊澤有話說 出處: //www.cnblogs.com/xiongze520/p/15540035.html 您可以隨意轉載、摘錄,但請在文章內註明作者和原文鏈接。
|