緣起
哈嘍,老張的不定期更新的日常又開始了,在咱們的前后端分離的.net core 框架中,雖然已經(jīng)實現(xiàn)了權(quán)限驗證《框架之五 || Swagger的使用 3.3 JWT權(quán)限驗證【修改】》,只不過還是有一些遺留問題,最近有不少的小伙伴發(fā)現(xiàn)了這樣的一些問題,本來想著直接就在原文修改,但是發(fā)現(xiàn)可能怕有的小伙伴看不到,就單發(fā)一條推送吧,所以我還是單寫出一篇文章來說明解決這些問題,希望對無論是正在開發(fā)權(quán)限管理系統(tǒng),還是平時需要數(shù)據(jù)庫動態(tài)綁定權(quán)限分配的你有一些啟發(fā)和思考。今天咱們注意解決這三個問題:
1、過期時間無效;
2、權(quán)限策略是寫死的,如何存入數(shù)據(jù)庫;
3、如何進(jìn)行無狀態(tài)權(quán)限驗證;
之前我也是考慮了一些時間,但是都不是很好的方法,就一直擱淺,正好群里一個大神提供了很好的方法,今天我就不敢用完美來形容了,怕有人批評,嘩眾取寵,因為是上一個系列,而且也是老問題,這里就不過多的進(jìn)行文字介紹了,直接上代碼。
投稿作者:這里重點說明下,是參考QQ群里小伙伴 Demon @忐-忑 的相關(guān)內(nèi)容,基本都是他的功勞,我只是一個搬運工??。
預(yù)告: 關(guān)于復(fù)雜的詳細(xì)的權(quán)限驗證系列,我會在DDD領(lǐng)域驅(qū)動設(shè)計之后,開啟這個基于微服務(wù)的 IdentityServer4 系列講解,這里先預(yù)告一下。
一、解決過期問題
在之前的代碼里,JWT 雖然已經(jīng)可以實現(xiàn)驗證了,但是卻無法達(dá)到過期時間,這個也是一個不大不小的問題,以前之所以無法實現(xiàn)這個功能,主要是犯了兩個小錯誤
1、沒有真正用到JWT的Bearer驗證;
2、使用了自定義的授權(quán),而沒有用官方UseAuthentication授權(quán),導(dǎo)致過期時間沒有生效;
這里就調(diào)整下代碼:
1、重新設(shè)計 IssueJWT 生成 Token 的方法
/// <summary>
/// 頒發(fā)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的默認(rèn)配置
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,我們就不用手動設(shè)置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的權(quán)限驗證服務(wù)
//認(rèn)證
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,//是否驗證超時 當(dāng)設(shè)置exp和nbf時有效 同時啟用ClockSkew
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtHelper.secretKey)), //注意這是緩沖過期時間,總的有效時間等于這個時間加上jwt的過期時間,如果不配置,默認(rèn)是5分鐘
ClockSkew = TimeSpan.FromSeconds(30)
};
});
其實和之前的方法是一樣的,只不過請注意 ClockSkew 屬性,默認(rèn)是5分鐘緩沖。
總的Token有效時間 = JwtRegisteredClaimNames.Exp + ClockSkew ;
3、啟動權(quán)限認(rèn)證配置
在之前的方法中,我們用到了中間件 app.UseMiddleware<JwtTokenAuth>(); 當(dāng)然也是可以的,只不過授權(quán)的時候?qū)懙牟蝗?,才?dǎo)致驗證的時候有效時間沒辦法識別,因為我們在生成Token的時候,已經(jīng)配置好了 claim 聲明,所以直接調(diào)用官方的驗證即可。這樣的好處是,我們也不用去判斷 Headers 是否包含 Authorization 的操作;
//app.UseMiddleware<JwtTokenAuth>();//注意此授權(quán)方法已經(jīng)放棄,請使用下邊的官方授權(quán)方法。這里僅僅是授權(quán)方法的替換
app.UseAuthentication();
雖然這個時候我們放棄了使用中間件來授權(quán),但是通過大家的學(xué)習(xí),已經(jīng)完全掌握了中間件的使用了吧,也算是對中間件的一個學(xué)習(xí)過程,因為在其他地方繼續(xù)使用其他的中間件。
重要:
這里使用 app.UseAuthentication(); 的目的是為了替換授權(quán)方法,如果你仍需要中間件傳值的話,比如把用戶信息寫入全局,請繼續(xù)使用中間件!
4、重要:正確的Token輸入方法
在之前中,我犯了一個想當(dāng)然的錯誤,然后就直接是解析的 Token 字符串,獲取到數(shù)據(jù),這個自然是沒有錯的,只不過這樣就無法正常的使用認(rèn)證服務(wù)中的 AddJwtBearer 方法。那該怎么辦呢,很簡單,就是以后在 Http請求的時候,帶上Bearer(空格)Token,這樣的格式,比如:Bearer 96sdfoysgoi79d87g.sd0ug97sdgf15fdg4531dfg


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

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

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

注意:在使用 Policy 的時候,以前我寫的有問題,請注意修改
services.AddAuthorization(options => {
options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());
options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); //這個寫法是錯誤的,這個是并列的關(guān)系,不是或的關(guān)系 //options.AddPolicy("AdminOrClient", policy => policy.RequireRole("Admin,Client").Build()); //這個才是或的關(guān)系
options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));
});
四、權(quán)限管理系統(tǒng)Id4
這個系列我會在DDD領(lǐng)域驅(qū)動設(shè)計之后,開啟這個 IdentityServer4 系列講解,這里先預(yù)告一下。