ASP.NET Core 3.1使用JWT認證Token授權
0、引言
若不清楚什麼是JWT的請先了解下什麼是JWT。
1、關於Authentication與Authorization
我相信在aspnet core中剛接觸甚至用了段時間這兩個概念的時候都是一頭霧水的,傻傻分不清。
認證(Authentication)和授權(Authorization)在概念上比較的相似,且又有一定的聯絡,因此很容易混淆。
認證(Authentication)是指驗證使用者身份的過程,即當用戶要訪問受保護的資源時,將其資訊(如使用者名稱和密碼)傳送給伺服器並由伺服器驗證的過程。
授權(Authorization)是驗證一個已通過身份認證的使用者是否有許可權做某件事情的過程。
有過RBAC的開發經驗者來說這裡可以這麼通俗的來理解:認證是驗證一個使用者是否“合法”(一般就是檢查資料庫中是否有這麼個使用者),授權是驗證這個使用者是否有做事情的許可權(簡單理解成RBAC中的使用者許可權)。
2、整個認證流程是怎樣的?
從圖中可以看到整個認證、授權的流程,先進行身份驗證 ,驗證通過後將Token放回給客戶端,客戶端訪問資源的時候請求頭中新增Token資訊,伺服器進行驗證並於授權是否能夠訪問該資源。
3、開始JWT身份認證
3.1引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer
3.2 在Startup.cs檔案中進行配置
#region jwt驗證 var jwtConfig = new JwtConfig(); Configuration.Bind("JwtConfig", jwtConfig); services .AddAuthentication(option => { //認證middleware配置 option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { //Token頒發機構 ValidIssuer = jwtConfig.Issuer, //頒發給誰 ValidAudience = jwtConfig.Audience, //這裡的key要進行加密 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.SecretKey)), //是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比 ValidateLifetime = true, }; }); #endregion
配置中介軟體
//啟用身份驗證功能。必須要在app.UseAuthorization();之前 app.UseAuthentication();//鑑權,
JWTHelper
public class JWTHelper { public class JwtHelper { /// <summary> /// 頒發JWT字串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJwt(TokenModelJwt tokenModel) { // 自己封裝的 appsettign.json 操作類,看下文 string iss = Appsettings.app(new string[] { "Audience", "Issuer" }); string aud = Appsettings.app(new string[] { "Audience", "Audience" }); string secret = Appsettings.app(new string[] { "Audience", "Secret" }); var claims = new List<Claim> { /* * 特別重要: 1、這裡將使用者的部分資訊,比如 uid 存到了Claim 中,如果你想知道如何在其他地方將這個 uid從 Token 中取出來,請看下邊的SerializeJwt() 方法,或者在整個解決方案,搜尋這個方法,看哪裡使用了! 2、你也可以研究下 HttpContext.User.Claims ,具體的你可以看看 Policys/PermissionHandler.cs 類中是如何使用的。 */ new Claim(JwtRegisteredClaimNames.Jti, tokenModel.Uid.ToString()), new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //這個就是過期時間,目前是過期7200秒,可自定義,注意JWT有自己的緩衝過期時間 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(7200)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Iss,iss), new Claim(JwtRegisteredClaimNames.Aud,aud), //new Claim(ClaimTypes.Role,tokenModel.Role),//為了解決一個使用者多個角色(比如:Admin,System),用下邊的方法 }; // 可以將一個使用者的多個角色全部賦予; // 作者:DX 提供技術支援; claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //祕鑰 (SymmetricSecurityKey 對安全性的要求,金鑰的長度太短會報出異常) var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: iss, claims: claims, signingCredentials: creds //,expires:DateTime.Now.AddMinutes(1) ); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// <summary> /// 解析 /// </summary> /// <param name="jwtStr"></param> /// <returns></returns> public static TokenModelJwt SerializeJwt(string jwtStr) { var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); object role; try { jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role); } catch (Exception e) { Console.WriteLine(e); throw; } var tm = new TokenModelJwt { Uid = (jwtToken.Id).ObjToInt(), Role = role != null ? role.ObjToString() : "", }; return tm; } } /// <summary> /// 令牌 /// </summary> public class TokenModelJwt { /// <summary> /// Id /// </summary> public long Uid { get; set; } /// <summary> /// 角色 /// </summary> public string Role { get; set; } /// <summary> /// 職能 /// </summary> public string Work { get; set; } } }
然後呢 如果你使用的Swagger
加上自動在Header帶上Token
Bearer就是在ASP.NET Core 在 Microsoft.AspNetCore.Authentication 下實現了一系列認證, 包含 Cookie, JwtBearer, OAuth, OpenIdConnect 等
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { Description = "JWT授權(資料將在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間有一個空格)", Name = "Authorization",//jwt預設的引數名稱, In = ParameterLocation.Header,//jwt預設存放Autorization資訊的位置(header中) Type = SecuritySchemeType.ApiKey, //指定ApiKey BearerFormat = "JWT",//標識承載令牌的格式 該資訊主要是出於文件目的 Scheme = "bearer"//授權中要使用的HTTP授權方案的名稱 }); //在Heder中新增Token 傳遞到後臺 options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme{ Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer"} },new string[] { } } });
在對應Controller上或Action上加上Authorize標記
驗證TOken
options.Events = new JwtBearerEvents { //此處為許可權驗證失敗後觸發的事件 OnChallenge = context => { //此處程式碼為終止.Net Core預設的返回型別和資料結果,這個很重要哦,必須 context.HandleResponse(); //自定義自己想要返回的資料結果,我這裡要返回的是Json物件,通過引用Newtonsoft.Json庫進行轉換 var payload = JsonConvert.SerializeObject(new ApiResult<bool>("Token無效")); //自定義返回的資料型別 context.Response.ContentType = "application/json"; //自定義返回狀態碼,預設為401 我這裡改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //context.Response.StatusCode = StatusCodes.Status401Unauthorized; //輸出Json資料結果 context.Response.WriteAsync(payload); return Task.FromResult(0); } };
/// <summary> /// 獲取Token /// </summary> /// <param name="req">請求流</param> /// <returns></returns> public static string GetToken(HttpRequest req) { string tokenHeader = req.Headers["Authorization"].ToString(); if (string.IsNullOrEmpty(tokenHeader)) throw new Exception("缺少token!"); string pattern = "^Bearer (.*?)$"; if (!Regex.IsMatch(tokenHeader, pattern)) throw new Exception("token格式不對!格式為:Bearer {token}"); string token = Regex.Match(tokenHeader, pattern).Groups[1]?.ToString(); if (string.IsNullOrEmpty(token)) throw new Exception("token不能為空!"); return token; }
好了 基本沒問題 最後在如果請求401可以自定義
整的程式碼如下:
[AllowAnonymous] [HttpGet] [Route("api/auth")] public IActionResult Get(string userName, string pwd) { if (CheckAccount(userName, pwd, out string role)) { //每次登陸動態重新整理 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(30)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.NameIdentifier, userName), new Claim("Role", role) }; //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, //過期時間 expires: DateTime.Now.AddMinutes(30), //簽名證書 signingCredentials: creds, //自定義引數 claims: claims ); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } }
3.3 然後改造一下StartUp.cs
我們僅僅需要關心改動的地方,也就是AddJwtBearer這個驗證token的方法,我們不用原先的固定值的校驗方式,而提供一個代理方法進行執行時執行校驗
.AddJwtBearer(options => options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = true,//是否驗證失效時間 ClockSkew = TimeSpan.FromSeconds(30), ValidateAudience = true,//是否驗證Audience //ValidAudience = Const.GetValidudience(),//Audience //這裡採用動態驗證的方式,在重新登陸時,重新整理token,舊token就強制失效了 AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//是否驗證Issuer ValidIssuer = Const.Domain,//Issuer,這兩項和前面簽發jwt的設定一致 ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey };
這裡邏輯是這樣的:因為重新登陸將原來的變數更改了,所以這裡校驗的時候也一併修改成了新的變數值,那麼舊的token當然就不匹配了,也就是舊的token被強制失效了。
參考地址https://www.cnblogs.com/7tiny/p/11012035.html https://www.cnblogs.com/cokeking/p/10969579.html