緣起
哈嘍,老張的不定期更新的日常又開始了,在咱們的前后端分離的.net core 框架中,雖然已經實現了權限驗證《框架之五 || Swagger的使用 3.3 JWT權限驗證【修改】》,只不過還是有一些遺留問題,最近有不少的小伙伴發現了這樣的一些問題,本來想着直接就在原文修改,但是發現可能怕有的小伙伴看不到,就單發一條推送吧,所以我還是單寫出一篇文章來說明解決這些問題,希望對無論是正在開發權限管理系統,還是平時需要數據庫動態綁定權限分配的你有一些啟發和思考。今天咱們注意解決這三個問題:
1、過期時間無效;
2、權限策略是寫死的,如何存入數據庫;
3、如何進行無狀態權限驗證;
之前我也是考慮了一些時間,但是都不是很好的方法,就一直擱淺,正好群里一個大神提供了很好的方法,今天我就不敢用完美來形容了,怕有人批評,嘩眾取寵,因為是上一個系列,而且也是老問題,這里就不過多的進行文字介紹了,直接上代碼。
投稿作者:這里重點說明下,是參考QQ群里小伙伴 Demon @忐-忑 的相關內容,基本都是他的功勞,我只是一個搬運工😀。
關於JWT一共三篇 姊妹篇,內容分別從簡單到復雜,一定要多看多想:
預告: 關於復雜的詳細的權限驗證系列,我會在DDD領域驅動設計之后,開啟這個基於微服務的 IdentityServer4 系列講解,這里先預告一下。
一、解決過期問題
在之前的代碼里,JWT 雖然已經可以實現驗證了,但是卻無法達到過期時間,這個也是一個不大不小的問題,以前之所以無法實現這個功能,主要是犯了兩個小錯誤
1、沒有真正用到JWT的Bearer驗證;
2、使用了自定義的授權,而沒有用官方UseAuthentication授權,導致過期時間沒有生效;
這里就調整下代碼:
1、重新設計 IssueJWT 生成 Token 的方法
/// <summary> /// 頒發JWT字符串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJWT(TokenModelJWT tokenModel) { var dateTime = DateTime.UtcNow;
//var claims = new Claim[] //{ // new Claim(JwtRegisteredClaimNames.Jti,tokenModel.Uid.ToString()),//Id // new Claim("Role", tokenModel.Role),//角色 // new Claim(JwtRegisteredClaimNames.Iat,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),
new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(10)).ToUnixTimeSeconds()}") //}; var claims = new Claim[] { //下邊為Claim的默認配置 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //這個就是過期時間,目前是過期100秒,可自定義,注意JWT有自己的緩沖過期時間 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(100)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Iss,"Blog.Core"), new Claim(JwtRegisteredClaimNames.Aud,"wr"), //這個Role是官方UseAuthentication要要驗證的Role,我們就不用手動設置Role這個屬性了 new Claim(ClaimTypes.Role,tokenModel.Role), }; //秘鑰 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtHelper.secretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: "Blog.Core", claims: claims, signingCredentials: creds); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; }
主要的修改,就是Claim[]的聲明上,定義了過期時間和Role。
2、修改JWT的權限驗證服務
//認證 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否驗證Issuer ValidateAudience = true,//是否驗證Audience ValidateIssuerSigningKey = true,//是否驗證IssuerSigningKey ValidIssuer = "Blog.Core", ValidAudience = "wr", ValidateLifetime = true,//是否驗證超時 當設置exp和nbf時有效 同時啟用ClockSkew IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtHelper.secretKey)),//從appsettings.json拿到的密鑰Secret,更多內容往下看 //注意這是緩沖過期時間,總的有效時間等於這個時間加上jwt的過期時間,如果不配置,默認是5分鍾 ClockSkew = TimeSpan.FromSeconds(30) }; });
更新:注意上邊的代碼在Github中已經有了修改,基本內容都一樣,只是位置微調了,說白了,就是把這一塊封裝了一個實例罷了,不會看不懂:
// 2.1【認證】、官方JWT認證 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = tokenValidationParameters; }); //上邊用到的 tokenValidationParameters //讀取配置文件 var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = audienceConfig["Secret"]; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); // 令牌驗證參數 var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey,//還是從 appsettings.json 拿到的 ValidateIssuer = true, ValidIssuer = audienceConfig["Issuer"],//發行人 ValidateAudience = true, ValidAudience = audienceConfig["Audience"],//訂閱人 ValidateLifetime = true, ClockSkew = TimeSpan.Zero, RequireExpirationTime = true, };
其實和之前的方法是一樣的,只不過請注意 ClockSkew 屬性,默認是5分鍾緩沖。
總的Token有效時間 = JwtRegisteredClaimNames.Exp + ClockSkew ;
3、啟動權限認證配置
在之前的方法中,我們用到了中間件 app.UseMiddleware<JwtTokenAuth>(); 當然也是可以的,只不過授權的時候寫的不全,才導致驗證的時候有效時間沒辦法識別,因為我們在生成Token的時候,已經配置好了 claim 聲明,所以直接調用官方的驗證即可。這樣的好處是,我們也不用去判斷 Headers 是否包含 Authorization 的操作;
//app.UseMiddleware<JwtTokenAuth>();//注意此授權方法已經放棄,請使用下邊的官方授權方法。這里僅僅是授權方法的替換 app.UseAuthentication();
雖然這個時候我們放棄了使用中間件來授權,但是通過大家的學習,已經完全掌握了中間件的使用了吧,也算是對中間件的一個學習過程,因為在其他地方繼續使用其他的中間件。
重要:
這里使用 app.UseAuthentication(); 的目的是為了替換授權方法,如果你仍需要中間件傳值的話,比如把用戶信息寫入全局,請繼續使用中間件!
4、重要:正確的Token輸入方法
在之前中,我犯了一個想當然的錯誤,然后就直接是解析的 Token 字符串,獲取到數據,這個自然是沒有錯的,只不過這樣就無法正常的使用認證服務中的 AddJwtBearer 方法。那該怎么辦呢,很簡單,就是以后在 Http請求的時候,帶上Bearer(空格)Token,這樣的格式,比如:Bearer 96sdfoysgoi79d87g.sd0ug97sdgf15fdg4531dfg


5、測試接口,查看是否有效
這個時候我們等待130秒,就可以看到已經過期了,如果你沒有明白為啥是130秒,請看上文

二、把驗證策略寫到數據庫
其實之前我已經在數據庫表結構中,配置了用到的數據庫表,只不過一直沒有用,
├── Module // 菜單表 ├── ModulePermission // 菜單與按鈕關系表 ├── Permission // 按鈕表 ├── Role // 角色表 ├── RoleModulePermission // 按鈕跟權限關聯表 ├── UserRole // 用戶跟角色關聯表 └── sysUserInfo // 用戶信息表
目前我采用的是,直接獲取當前用戶的全部角色信息,賦值給 JWT 的Token,然后通過 UseAuthentication() 進行授權
//獲取當前用戶全部的角色信息(字符串,逗號隔開) var userRoles = await sysUserInfoServices.GetUserRoleNameStr(name, pass); if (user != null) { TokenModelJWT tokenModel = new TokenModelJWT(); tokenModel.Uid = 1; tokenModel.Role = user; }
這里先留下一個坑,以后再開發權限管理系統的時候,再單寫一個系統吧。
三、無狀態與有狀態驗證
1、無狀態授權
在第一部分中,我們不僅已經實現了Token的有效期,而且自然而然是實現了授權驗證,只需要在知道的Controller 或者方法上增加特性 [Authorize] 就可以實現驗證

過程是這樣的,我們登陸,認證用戶信息,成功后,分發Role,然后生成 Token ,這個時候就已經代表當前用戶是有有權限的,只不過是無狀態的,我們不知道他的具體是什么角色,但是會被 app.UseAuthentication(); 識別並通過,如果我們僅僅想給接口增加一個驗證,而不要求角色信息,就可以這么操作。
如果想在授權的controller中,讓某一個方法可以讓所有人訪問,可以增加 [AllowAnonymous] 特性;
[HttpGet("{id}")] [AllowAnonymous]//不受授權控制,任何人都可訪問 public ActionResult<string> Get(int id) { return "value"; }
那這個時候你會問,我如果就想要當前用戶必須是某一個Role才能訪問呢,請往下看。
2、有角色授權
這個時候我們就需要增加 Role 信息了,比如這樣:

注意:在使用 Policy 的時候,以前我寫的有問題,請注意修改
services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build()); options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); //這個寫法是錯誤的,這個是並列的關系,不是或的關系 //options.AddPolicy("AdminOrClient", policy => policy.RequireRole("Admin,Client").Build()); //這個才是或的關系 options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System")); });
四、權限管理系統Id4
這個系列我會在DDD領域驅動設計之后,開啟這個 IdentityServer4 系列講解,這里先預告一下。
系列已經開啟:
《從壹開始 [ Id4 ] 之一║ 授權服務器 IdentityServer4 開篇講&計划書》
