什麼是JWT
JSON Web Token(JWT)是目前最流行的跨域身份驗證、分散式登入、單點登入等解決方案。
JWT的官網地址:https://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(一):https://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
- 五分鐘帶你瞭解啥是JWT:https://zhuanlan.zhihu.com/p/86937325
- C#分散式登入——jwt:https://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 = "https://localhost:44345"; /// <summary> /// 受理人,之所以弄成可變的是為了用介面動態更改這個值以模擬強制Token失效 /// 真實業務場景可以在資料庫或者redis存一個和使用者id相關的值,生成token和驗證token的時候獲取到持久化的值去校驗 /// 如果重新登陸,則重新整理這個值 /// </summary> public static string ValidAudience; } }
JWT登入授權測試成功
把程式編譯執行起來,開啟postman,輸入地址,首先測試不需要任何授權的
正確地返回了資料,那麼接下來我們測試JWT的流程。
首先我們什麼都不加呼叫介面:https://localhost:44345/api/values2,注意,我建立的時候是https的,大家注意看是http還是https
返回了狀態碼401,也就是未經授權:訪問由於憑據無效被拒絕。 說明JWT校驗生效了,我們的介面收到了保護。
呼叫模擬登陸授權介面:https://localhost:44345/api/auth?userName=xiongze&pwd=123456
這裡的使用者密碼是隨便寫的,因為我們模擬登陸只是校驗了下非空,因此寫什麼都能通過。
然後我們得到了一個xxx.yyy.zzz 格式的 token 值。我們把token複製出來。
在剛才401的介面(https://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}" }; }
不同許可權測試訪問
我們同樣的方法去模擬登入,https://localhost:44345/api/auth?userName=xiongze&pwd=123
注意,賬號先不用admin登入,然後用返回的token去請求我們剛剛新增的指定許可權訪問的介面,這個時候是沒有許可權訪問的,因為這個是admin許可權訪問。
我們同樣的方法去模擬登入,https://localhost:44345/api/auth?userName=admin&pwd=123
訪問成功。
原始碼下載地址
Gitee:https://gitee.com/xiongze/jwtWebAPI.git
參考文獻
- asp.net core 整合JWT(一):https://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
- 五分鐘帶你瞭解啥是JWT:https://zhuanlan.zhihu.com/p/86937325
- C#分散式登入——jwt:https://www.cnblogs.com/yswenli/p/13510050.html
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂 公眾號:熊澤有話說 出處: https://www.cnblogs.com/xiongze520/p/15540035.html 您可以隨意轉載、摘錄,但請在文章內註明作者和原文連結。
|