傳統Session所暴露的問題
Session: 用戶每次在計算機身份認證之后,在服務器內存中會存放一個session,在客戶端會保存一個cookie,以便在下次用戶請求時進行身份核驗。但是這樣就暴露了兩個問題。第一個問題是,session是存儲到服務器的內存中,當請求的用戶數量增加時,會加重服務器的壓力。第二個問題是,若是有多台服務器,而session只能存儲到當前的某一台服務器中,這就不適用於分布式開發。
CSRF: Session是基於cookie來進行用戶識別的,如果cookie被截獲,用戶就很容易受到跨站請求偽造攻擊,本文暫時不考慮csrf(cross site request forgery)。
Token的驗證機制
token的驗證不需要在服務器端保留任何的用戶信息,因此,當用戶再客戶端通過單點登陸后,可以訪問多台服務器,利於分布式開發。而且token的是一串加密后的字符串,可以設置過期日期,不容易被仿造。
使用token,客戶端和服務端的交互流程大致是如下:
- 用戶使用用戶名密碼來請求服務器
- 服務器進行驗證用戶的信息
- 服務器通過驗證發送給用戶一個token
- 客戶端存儲token,並在每次請求時附送上這個token值
- 服務端驗證token值,並返回數據
token可以存放在cookie中,也可以保存在請求頭中,建議將token放到請求頭中,並且token攜帶mac地址和機器名。
注意
使用JWT盡量使用HTTPS協議 避免挾持
JWT 有個問題,導致很多開發團隊放棄使用它,那就是一旦頒發一個 JWT 令牌,服務端就沒辦法廢棄掉它,除非等到它自身過期。有很多應用默認只允許最新登錄的一個客戶端正常使用,不允許多端登錄,JWT 就沒辦法做到,因為頒發了新令牌,但是老的令牌在過期前仍然可用。這種情況下,就需要服務端增加相應的邏輯。
Jwt刷新系統 登錄的時候存兩個token 一個刷新token存7天 一個業務token30分鍾 每次請求前判斷業務token是否過期 或者臨近過期 就去用刷新token和業務token請求相應接口刷新 返回業務token 更新在前端 在發送請求;有點麻煩
或者單系統 不管 直接存1天 1天后到期重新登錄
刷新Token方案
方案1.后端存儲最后一次token,后端判斷toekn,合法超時,刷新token推給前端 比如放在response的heard中
優點:前端改動小
缺點:后端實現復雜,需要而外存儲。
方案2.
前端解碼token。拿到過期時間,和當前時間進行判斷。如果快過期,主動調用獲取新token.
缺點:前端每次請求需要解碼判斷
優點:后端壓力小,不需要存儲。
在 Web 應用發展的初期,大部分采用基於 Session 的會話管理方式,邏輯如下。
- 客戶端使用用戶名密碼進行認證
- 服務端生成並存儲 Session,將 SessionID 通過 Cookie 返回給客戶端
- 客戶端訪問需要認證的接口時在 Cookie 中攜帶 SessionID
- 服務端通過 SessionID 查找 Session 並進行鑒權,返回給客戶端需要的數據
基於 Session 的方式存在多種問題。
- 服務端需要存儲 Session,並且由於 Session 需要經常快速查找,通常存儲在內存或內存數據庫中,同時在線用戶較多時需要占用大量的服務器資源。
- 當需要擴展時,創建 Session 的服務器可能不是驗證 Session 的服務器,所以還需要將所有 Session 單獨存儲並共享。
- 由於客戶端使用 Cookie 存儲 SessionID,在跨域場景下需要進行兼容性處理,同時這種方式也難以防范 CSRF 攻擊。
基於 Token 的會話管理
鑒於基於 Session 的會話管理方式存在上述多個缺點,無狀態的基於 Token 的會話管理方式誕生了,所謂無狀態,就是服務端不再存儲信息,甚至是不再存儲 Session,邏輯如下。
- 客戶端使用用戶名密碼進行認證
- 服務端驗證用戶名密碼,通過后生成 Token 返回給客戶端
- 客戶端保存 Token,訪問需要認證的接口時在 URL 參數或 HTTP Header 中加入 Token
- 服務端通過解碼 Token 進行鑒權,返回給客戶端需要的數據
JWT 優勢 & 問題
JWT 擁有基於 Token 的會話管理方式所擁有的一切優勢,不依賴 Cookie,使得其可以防止 CSRF 攻擊,也能在禁用 Cookie 的瀏覽器環境中正常運行。
而 JWT 的最大優勢是服務端不再需要存儲 Session,使得服務端認證鑒權業務可以方便擴展,避免存儲 Session 所需要引入的 Redis 等組件,降低了系統架構復雜度。但這也是 JWT 最大的劣勢,由於有效期存儲在 Token 中,JWT Token 一旦簽發,就會在有效期內一直可用,無法在服務端廢止,當用戶進行登出操作,只能依賴客戶端刪除掉本地存儲的 JWT Token,如果需要禁用用戶,單純使用 JWT 就無法做到了。
基於 JWT 的實踐
既然 JWT 依然存在諸多問題,甚至無法滿足一些業務上的需求,但是我們依然可以基於 JWT 在實踐中進行一些改進,來形成一個折中的方案,畢竟,在用戶會話管理場景下,沒有銀彈。
前面講的 Token,都是 Access Token,也就是訪問資源接口時所需要的 Token,還有另外一種 Token,Refresh Token,通常情況下,Refresh Token 的有效期會比較長,而 Access Token 的有效期比較短,當 Access Token 由於過期而失效時,使用 Refresh Token 就可以獲取到新的 Access Token,如果 Refresh Token 也失效了,用戶就只能重新登錄了。
在 JWT 的實踐中,引入 Refresh Token,將會話管理流程改進如下。
- 客戶端使用用戶名密碼進行認證
- 服務端生成有效時間較短的 Access Token(例如 10 分鍾),和有效時間較長的 Refresh Token(例如 7 天)
- 客戶端訪問需要認證的接口時,攜帶 Access Token
- 如果 Access Token 沒有過期,服務端鑒權后返回給客戶端需要的數據
- 如果攜帶 Access Token 訪問需要認證的接口時鑒權失敗(例如返回 401 錯誤),則客戶端使用 Refresh Token 向刷新接口申請新的 Access Token
- 如果 Refresh Token 沒有過期,服務端向客戶端下發新的 Access Token
- 客戶端使用新的 Access Token 訪問需要認證的接口
將生成的 Refresh Token 以及過期時間存儲在服務端的數據庫中,由於 Refresh Token 不會在客戶端請求業務接口時驗證,只有在申請新的 Access Token 時才會驗證,所以將 Refresh Token 存儲在數據庫中,不會對業務接口的響應時間造成影響,也不需要像 Session 一樣一直保持在內存中以應對大量的請求。
上述的架構,提供了服務端禁用用戶 Token 的方式,當用戶需要登出或禁用用戶時,只需要將服務端的 Refresh Token 禁用或刪除,用戶就會在 Access Token 過期后,由於無法獲取到新的 Access Token 而再也無法訪問需要認證的接口。這樣的方式雖然會有一定的窗口期(取決於 Access Token 的失效時間),但是結合用戶登出時客戶端刪除 Access Token 的操作,基本上可以適應常規情況下對用戶認證鑒權的精度要求。
總結
JWT 的使用,提高了開發者開發用戶認證鑒權功能的效率,降低了系統架構復雜度,避免了大量的數據庫和緩存查詢,降低了業務接口的響應延遲。然而 JWT 的這些優點也增加了 Token 管理上的難度,通過引入 Refresh Token,既能繼續使用 JWT 所帶來的優勢,又能使得 Token 管理的精度符合業務的需求。
.Net使用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被強制失效了。
最新的另一種刷新token
參考地址https://www.cnblogs.com/7tiny/p/11012035.html https://www.cnblogs.com/cokeking/p/10969579.html