【 .NET Core 3.0 】框架之五 || JWT权限验证
前言
關(guān)于JWT一共三篇 姊妹篇,內(nèi)容分別從簡(jiǎn)單到復(fù)雜,一定要多看多想:
? ? ? 一、Swagger的使用 3.3 JWT權(quán)限驗(yàn)證【修改】
? ? ? 二、解決JWT權(quán)限驗(yàn)證過期問題
? ? ? 三、JWT完美實(shí)現(xiàn)權(quán)限與接口的動(dòng)態(tài)分配
?這里一共三個(gè)文章,目前是第一篇,剩下兩篇主要是在博客園,大家點(diǎn)擊閱讀原文,自行查看就行。
本文有配套視頻:https://www.bilibili.com/video/av58096866/?p=4
1、如何給接口實(shí)現(xiàn)權(quán)限驗(yàn)證?
其實(shí)關(guān)于這一塊,我思考了下,因?yàn)楫吘刮业捻?xiàng)目中是使用的vue + api 搭建一個(gè)前臺(tái)展示,大部分頁(yè)面都沒有涉及到權(quán)限驗(yàn)證,本來要忽略這一章節(jié),可是猶豫再三,還是給大家簡(jiǎn)單分析了下,個(gè)人還是希望陪大家一直搭建一個(gè)較為強(qiáng)大的,只要是涉及到后端那一定就需要?登錄=》驗(yàn)證了
根據(jù)維基百科定義,JWT(讀作 [/d??t/]),即JSON Web Tokens,是一種基于JSON的、用于在網(wǎng)絡(luò)上聲明某種主張的令牌(token)。JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。它是一種用于雙方之間傳遞安全信息的表述性聲明規(guī)范。JWT作為一個(gè)開放的標(biāo)準(zhǔn)(RFC 7519),定義了一種簡(jiǎn)潔的、自包含的方法,從而使通信雙方實(shí)現(xiàn)以JSON對(duì)象的形式安全的傳遞信息。
以上是JWT的官方解釋,可以看出JWT并不是一種只能權(quán)限驗(yàn)證的工具,而是一種標(biāo)準(zhǔn)化的數(shù)據(jù)傳輸規(guī)范。所以,只要是在系統(tǒng)之間需要傳輸簡(jiǎn)短但卻需要一定安全等級(jí)的數(shù)據(jù)時(shí),都可以使用JWT規(guī)范來傳輸。規(guī)范是不因平臺(tái)而受限制的,這也是JWT做為授權(quán)驗(yàn)證可以跨平臺(tái)的原因。
如果理解還是有困難的話,我們可以拿JWT和JSON類比:
JSON是一種輕量級(jí)的數(shù)據(jù)交換格式,是一種數(shù)據(jù)層次結(jié)構(gòu)規(guī)范。它并不是只用來給接口傳遞數(shù)據(jù)的工具,只要有層級(jí)結(jié)構(gòu)的數(shù)據(jù)都可以使用JSON來存儲(chǔ)和表示。當(dāng)然,JSON也是跨平臺(tái)的,不管是Win還是Linux,.NET還是Java,都可以使用它作為數(shù)據(jù)傳輸形式。
1)客戶端向授權(quán)服務(wù)系統(tǒng)發(fā)起請(qǐng)求,申請(qǐng)獲取“令牌”。
2)授權(quán)服務(wù)根據(jù)用戶身份,生成一張專屬“令牌”,并將該“令牌”以JWT規(guī)范返回給客戶端
3)客戶端將獲取到的“令牌”放到http請(qǐng)求的headers中后,向主服務(wù)系統(tǒng)發(fā)起請(qǐng)求。主服務(wù)系統(tǒng)收到請(qǐng)求后會(huì)從headers中獲取“令牌”,并從“令牌”中解析出該用戶的身份權(quán)限,然后做出相應(yīng)的處理(同意或拒絕返回資源)
?
?
零、生成 Token 令牌
關(guān)于JWT授權(quán),其實(shí)過程是很簡(jiǎn)單的,大家其實(shí)這個(gè)時(shí)候靜下心想一想就能明白,這個(gè)就是四步走:
首先我們需要一個(gè)具有一定規(guī)則的 Token 令牌,也就是 JWT 令牌(比如我們的公司門禁卡),//登錄
然后呢,我們?cè)俣x哪些地方需要什么樣的角色(比如領(lǐng)導(dǎo)辦公室我們是沒辦法進(jìn)去的),//授權(quán)機(jī)制
接下來,整個(gè)公司需要定一個(gè)規(guī)則,就是如何對(duì)這個(gè) Token 進(jìn)行驗(yàn)證,不能隨便寫個(gè)字條,這樣容易被造假(比如我們公司門上的每一道刷卡機(jī)),//認(rèn)證方案
最后,就是安全部門,開啟認(rèn)證中間件服務(wù)(那這個(gè)服務(wù)可以關(guān)閉的,比如我們電影里看到的黑客會(huì)把這個(gè)服務(wù)給關(guān)掉,這樣整個(gè)公司安保就形同虛設(shè)了)。//開啟中間件
?
那現(xiàn)在我們就是需要一個(gè)具有一定規(guī)則的 Token 令牌,大家可以參考 JwtHelper 這個(gè)類:
這個(gè)實(shí)體類就是用來生成 Token 的,代碼記錄如下:
/// <summary> /// 頒發(fā)JWT字符串 /// </summary> /// <param name="tokenModel"></param> /// <returns></returns> public static string IssueJwt(TokenModelJwt tokenModel) { string iss = Appsettings.app(new string[] { "Audience", "Issuer" }); string aud = Appsettings.app(new string[] { "Audience", "Audience" }); string secret = AppSecretConfig.Audience_Secret_String; //var claims = new Claim[] //old var claims = new List<Claim> { /* * 特別重要: 1、這里將用戶的部分信息,比如 uid 存到了Claim 中,如果你想知道如何在其他地方將這個(gè) uid從 Token 中取出來,請(qǐng)看下邊的SerializeJwt() 方法,或者在整個(gè)解決方案,搜索這個(gè)方法,看哪里使用了! 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()}") , //這個(gè)就是過期時(shí)間,目前是過期1000秒,可自定義,注意JWT有自己的緩沖過期時(shí)間 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.Expiration, DateTime.Now.AddSeconds(1000).ToString()), new Claim(JwtRegisteredClaimNames.Iss,iss), new Claim(JwtRegisteredClaimNames.Aud,aud), //new Claim(ClaimTypes.Role,tokenModel.Role),//為了解決一個(gè)用戶多個(gè)角色(比如:Admin,System),用下邊的方法 }; // 可以將一個(gè)用戶的多個(gè)角色全部賦予; // 作者:DX 提供技術(shù)支持; claims.AddRange(tokenModel.Role.Split(',').Select(s => new Claim(ClaimTypes.Role, s))); //秘鑰 (SymmetricSecurityKey 對(duì)安全性的要求,密鑰的長(zhǎng)度太短會(huì)報(bào)出異常) 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); var jwtHandler = new JwtSecurityTokenHandler(); var encodedJwt = jwtHandler.WriteToken(jwt); return encodedJwt; } /// <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; } }這里邊有一個(gè) Appsettings 類,主要的作用是自動(dòng)讀取項(xiàng)目配置文件 appsettings.json 。
?
public class AppSecretConfig { private static string Audience_Secret = Appsettings.app(new string[] { "Audience", "Secret" }); private static string Audience_Secret_File = Appsettings.app(new string[] { "Audience", "SecretFile" }); public static string Audience_Secret_String => InitAudience_Secret(); private static string InitAudience_Secret() { var securityString = DifDBConnOfSecurity(Audience_Secret_File); if (!string.IsNullOrEmpty(Audience_Secret_File)&& !string.IsNullOrEmpty(securityString)) { return securityString; } else { return Audience_Secret; } } private static string DifDBConnOfSecurity(params string[] conn) { foreach (var item in conn) { try { if (File.Exists(item)) { return File.ReadAllText(item).Trim(); } } catch (System.Exception) { } } return conn[conn.Length - 1]; } }?
?
這個(gè)接口如何調(diào)用呢,很簡(jiǎn)單,就是我們的登錄api:
public async Task<object> GetJwtStr(string name, string pass) { string jwtStr = string.Empty; bool suc = false; // 獲取用戶的角色名,請(qǐng)暫時(shí)忽略其內(nèi)部是如何獲取的,可以直接用 var userRole="Admin"; 來代替更好理解。 var userRole = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); if (userRole != null) { // 將用戶id和角色名,作為單獨(dú)的自定義變量封裝進(jìn) token 字符串中。 TokenModelJwt tokenModel = new TokenModelJwt {Uid = 1, Role = userRole}; jwtStr = JwtHelper.IssueJwt(tokenModel);//登錄,獲取到一定規(guī)則的 Token 令牌 suc = true; } else { jwtStr = "login fail!!!"; } return Ok(new { success = suc, token = jwtStr }); }?
現(xiàn)在我們獲取到Token了,那如何進(jìn)行授權(quán)認(rèn)證呢,別著急,重頭戲馬上到來!
?
一、JWT授權(quán)認(rèn)證流程——自定義中間件
在之前的搭建中,swagger已經(jīng)基本成型,其實(shí)其功能之多,不是我這三篇所能寫完的,想要添加權(quán)限,先從服務(wù)開始
0、Swagger中開啟JWT服務(wù)
我們要測(cè)試 JWT 授權(quán)認(rèn)證,就必定要輸入 Token令牌,那怎么輸入呢,平時(shí)的話,我們可以使用 Postman 來控制輸入,就是在請(qǐng)求的時(shí)候,在 Header 中,添加Authorization屬性,
但是我們現(xiàn)在使用了 Swagger 作為接口文檔,那怎么輸入呢,別著急, Swagger 已經(jīng)幫我們實(shí)現(xiàn)了這個(gè)錄入 Token令牌的功能:
在startup.cs 中的 ConfigureServices? ->?AddSwaggerGen?服務(wù)中,增加以下代碼,注意是swagger服務(wù)內(nèi)部:
var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; services.AddSwaggerGen(c => { //遍歷出全部的版本,做文檔信息展示 typeof(ApiVersions).GetEnumNames().ToList().ForEach(version => { c.SwaggerDoc(version, new OpenApiInfo { // {ApiName} 定義成全局變量,方便修改 Version = version, Title = $"{ApiName} 接口文檔——Netcore 3.0", Description = $"{ApiName} HTTP API " + version, Contact = new OpenApiContact { Name = ApiName, Email = "Blog.Core@xxx.com", Url = new Uri("https://www.jianshu.com/u/94102b59cc2a") }, License = new OpenApiLicense { Name = ApiName, Url = new Uri("https://www.jianshu.com/u/94102b59cc2a") } }); c.OrderActionsBy(o => o.RelativePath); }); //就是這里 var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//這個(gè)就是剛剛配置的xml文件名 c.IncludeXmlComments(xmlPath, true);//默認(rèn)的第二個(gè)參數(shù)是false,這個(gè)是controller的注釋,記得修改 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//這個(gè)就是Model層的xml文件名 c.IncludeXmlComments(xmlModelPath); c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>(); #region Token綁定到ConfigureServices c.AddSecurityDefinition("oauth2",?new?OpenApiSecurityScheme { Description = "JWT授權(quán)(數(shù)據(jù)將在請(qǐng)求頭中進(jìn)行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間是一個(gè)空格)\"", Name = "Authorization",//jwt默認(rèn)的參數(shù)名稱 In = ParameterLocation.Header,//jwt默認(rèn)存放Authorization信息的位置(請(qǐng)求頭中) Type = SecuritySchemeType.ApiKey }); #endregion });
?
然后執(zhí)行代碼,就可以在 swagger/index.html 頁(yè)面里看到這個(gè)Token入口了:
?
?
大家點(diǎn)開,看到輸入框,在輸入Token的時(shí)候,需要在Token令牌的前邊加上Bearer (為什么要加這個(gè),下文會(huì)說明,請(qǐng)一定要注意看,一定要明白為啥要帶,因?yàn)樗婕暗搅耸裁词鞘跈?quán),什么是認(rèn)證,還要自定義認(rèn)證中間件還是官方認(rèn)證中間件的區(qū)別,請(qǐng)注意看下文),比如是這樣的:
但是請(qǐng)注意!如果你使用的是中間件 app.UseMiddleware<JwtTokenAuth>(),或者 app.UseJwtTokenAuth() 的時(shí)候(兩種寫法一樣) ,要是使用 Bearer xxxx傳值的時(shí)候,記得在中間件的方法中,把Token的 “Bearer 空格” 字符給截取掉,這樣的:
?
1:API接口授權(quán)策略
這里可以直接在api接口上,直接設(shè)置該接口所對(duì)應(yīng)的角色權(quán)限信息:
這個(gè)時(shí)候我們就需要對(duì)每一個(gè)接口設(shè)置對(duì)應(yīng)的 Roles 信息,但是如果我們的接口需要對(duì)應(yīng)多個(gè)角色的時(shí)候,我們就可以直接寫多個(gè):
?
這里有一個(gè)情況,如果角色多的話,不僅不利于我們閱讀,還可能在配置的時(shí)候少一兩個(gè)role,比如這個(gè) api接口1 少了一個(gè) system 的角色,再比如那個(gè) api接口2 把 Admin 角色寫成了 Adnin 這種不必要的錯(cuò)誤,真是很難受,那怎么辦呢,欸!這個(gè)時(shí)候就出現(xiàn)了基于策略的授權(quán)機(jī)制:
我們?cè)?ConfigureService 中可以這么設(shè)置:
// 1【授權(quán)】、這個(gè)和上邊的異曲同工,好處就是不用在controller中,寫多個(gè) roles 。 // 然后這么寫 [Authorize(Policy = "Admin")] services.AddAuthorization(options => { options.AddPolicy("Client", policy => policy.RequireRole("Client").Build()); options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build()); options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System")); });?
這樣的話,我們只需要在 controller 或者 action 上,直接寫策略名就可以了:
[HttpGet] [Authorize(Policy = "SystemOrAdmin")] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; }?
這樣我們的第一步就完成了。繼續(xù)走第二步,身份驗(yàn)證方案。
?關(guān)于授權(quán)認(rèn)證有兩種方式,可以使用官方的認(rèn)證方式,也可以使用自定義中間件的方法,具體請(qǐng)往下看,咱們先說說如何進(jìn)行自定義認(rèn)證。
?
2、自定義認(rèn)證之身份驗(yàn)證設(shè)置
上邊第一步中,咱們已經(jīng)對(duì)每一個(gè)接口api設(shè)置好了 授權(quán)機(jī)制 ,那這里就要開始認(rèn)證,咱們先看看如何實(shí)現(xiàn)自定義的認(rèn)證:
?
JwtTokenAuth,一個(gè)中間件,用來過濾每一個(gè)http請(qǐng)求,就是每當(dāng)一個(gè)用戶發(fā)送請(qǐng)求的時(shí)候,都先走這一步,然后再去訪問http請(qǐng)求的接口
?
?
?
?
前兩步咱們都完成了,從授權(quán)到自定義身份驗(yàn)證方案,就剩下最后一步,開啟中間件了。
?
3:開啟自定義認(rèn)證中間件,實(shí)現(xiàn)Http信道攔截
這個(gè)很簡(jiǎn)單,只需要在 startup.cs ->?Configure 中配置認(rèn)證中間件
?
4:開始測(cè)試
這個(gè)時(shí)候我們的自定義JWT授權(quán)認(rèn)證已經(jīng)結(jié)束了,我們開始測(cè)試,假設(shè)對(duì)某一個(gè) api接口設(shè)置了權(quán)限:
?
在我們沒有輸入 Token 的時(shí)候,點(diǎn)擊測(cè)試接口會(huì)報(bào)錯(cuò):
?
?
這個(gè)錯(cuò)誤很明顯,就是說我們沒有配置默認(rèn)的認(rèn)證方案,也沒有自定義身份驗(yàn)證方案,
但是這個(gè)時(shí)候我們?cè)龠M(jìn)行試驗(yàn):
剛剛上邊的情況是我們沒有輸入 Token ,但是如果我們輸入token呢?看看是不是又會(huì)報(bào)錯(cuò)?
?
?
我們發(fā)現(xiàn)了什么?!!沒有報(bào)錯(cuò),這是因?yàn)槭裁?#xff1f;欸,聰明的你應(yīng)該想到了,請(qǐng)往下看,什么是 聲明主體 ClaimsPrincipal 。
?
5、聲明主體 ClaimsPrincipal 是如何保存的?
在上邊,我們解決了一些問題,同時(shí)也出現(xiàn)了一個(gè)問題,就是為什么不輸入 Token 就報(bào)錯(cuò)了,而輸入了 Bearer xxxxxxxxxxx 這樣的Token 就不報(bào)錯(cuò)了呢?這里要說到 聲明主體的作用了。
就是我們上邊寫的自定義中間件,大家可以再來看看:
?
?這個(gè)時(shí)候你就應(yīng)該明白了吧,
1、首先我們自定義授權(quán)認(rèn)證,為啥可以不用進(jìn)行下邊截圖中官方認(rèn)證那一塊的配置:
?
?
因?yàn)檫@一塊官方的服務(wù),就等同于我們的自定義身份驗(yàn)證方案——中間件。
2、你應(yīng)該明白,為什么不輸入token的時(shí)候報(bào)錯(cuò),而輸入了就不報(bào)錯(cuò)了?
因?yàn)闆]有輸入的時(shí)候,直接 return了,并沒有在 httpContext 上下文中,進(jìn)行配置聲明主體?httpContext.User = principal 。
所以說,我們無論是自定義中間件的自定義身份驗(yàn)證方案,還是官方的認(rèn)證方案,只要我們的登錄了,也就是說,只要我們實(shí)現(xiàn)了某種規(guī)則:
?
這樣,就會(huì)觸發(fā)我們的內(nèi)部服務(wù),將當(dāng)前 token 所攜帶的信息,進(jìn)行自動(dòng)解碼,然后填充到聲明主體里(自定義中間件需要手動(dòng)配置,官方的自動(dòng)就實(shí)現(xiàn)該操作),
所以這個(gè)時(shí)候我們就可以輕松的拿到想到的東西,比如這里這些:
?
?
6、無策略依然授權(quán)錯(cuò)誤
上邊咱們說到了,如果我們自定義中間件的話,在中間件中,我們?cè)?Claims 添加了角色的相關(guān)權(quán)限:
而且很自然的在 接口中,也是分為兩種情況:要么沒有加權(quán)限,要么就是基于角色的加權(quán):
?
?但是如果這個(gè)時(shí)候,我們直接對(duì)接口增加 無任何策略 的加權(quán):
?
就是沒有任何的策略,我們登錄,然后添加 token,一看,還是報(bào)錯(cuò)了!
本來 [Authorize] 這種 無策略 的授權(quán),按理說只需要我們登錄了就可以了,不需要其他任何限制就可以訪問,但是現(xiàn)在依然報(bào)錯(cuò)401 ,證明我們的中間件并不能對(duì)這種方案起到效果,你可能會(huì)問,那帶有 Roles=“Admin” 的為啥可以呢?反而這種無策略的不行呢,我個(gè)人感覺可能還是中間件咱們?cè)O(shè)計(jì)的解決方案就是基于角色授權(quán)的那種,(我也再研究研究,看看能不能完善下這個(gè)自定義中間件,使它能適應(yīng)這個(gè) 無具體策略 的加權(quán)方案,但是可能寫到最后,就是無限接近官方的授權(quán)中間件了哈哈)。
這個(gè)時(shí)候我們發(fā)現(xiàn),自定義中間件還是挺麻煩的,但是你通過自己使用自定義授權(quán)中間件,不僅僅可以了解到中間件的使用,還可以了解 netcore 到底是如何授權(quán)的機(jī)制,但是我還是建議大家使用官方的認(rèn)證方案,畢竟他們考慮的很全面的。
?
那么如果我們想要用官方的認(rèn)證方案呢,要怎么寫呢?請(qǐng)往下看:
?
二、JWT授權(quán)認(rèn)證流程——官方認(rèn)證
上邊咱們說完了自定義中間件的形式,發(fā)現(xiàn)了也方便的地方,也有不方便之處,雖然靈活的使用了自定義身份驗(yàn)證,但是畢竟很受限,而且也無法對(duì)過期時(shí)間進(jìn)行判斷,以后的文章你會(huì)看到《36 ║解決JWT自定義中間件授權(quán)過期問題》,這里先不說,重點(diǎn)說說,如何通過官方認(rèn)證來實(shí)現(xiàn)。
1:API接口授權(quán)策略
和上邊自定義的過程一模一樣,略。
?
2、官方默認(rèn)認(rèn)證配置
在剛剛上邊,咱們說到了一個(gè)錯(cuò)誤,不知道還有沒有印象:
No authenticationScheme was specified, and there was no DefaultChallengeScheme found.?
就是這個(gè),自定義認(rèn)證中間件呢,就是前者,那官方的,就是后者 DefaultChallengeScheme;
?
很簡(jiǎn)單,只需要在 configureService 中,添加【統(tǒng)一認(rèn)證】即可:
#region 【第二步:配置認(rèn)證服務(wù)】 // 令牌驗(yàn)證參數(shù) var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, ValidateIssuer = true, ValidIssuer = audienceConfig["Issuer"],//發(fā)行人 ValidateAudience = true, ValidAudience = audienceConfig["Audience"],//訂閱人 ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), RequireExpirationTime = true, }; //2.1【認(rèn)證】、core自帶官方JWT認(rèn)證 // 開啟Bearer認(rèn)證 services.AddAuthentication("Bearer") // 添加JwtBearer服務(wù) .AddJwtBearer(o => { o.TokenValidationParameters = tokenValidationParameters; o.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { // 如果過期,則把<是否過期>添加到,返回頭信息中 if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; });上邊代碼中出現(xiàn)的部分參數(shù)定義(如果還看不懂,請(qǐng)看項(xiàng)目代碼):
//讀取配置文件 var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = AppSecretConfig.Audience_Secret_String; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);?
具體的每個(gè)配置的含義呢,我的代碼里都有,大家自己可以看看,都很簡(jiǎn)單。
劃重點(diǎn):我們就是用的這個(gè)官方默認(rèn)的方案,來替換了我們自定義中間件的身份驗(yàn)證方案,從而達(dá)到目的,說白了,就是官方封裝了一套方案,這樣我們就不用寫中間件了。
?
3、配置官方認(rèn)證中間件
這個(gè)很簡(jiǎn)單,還是在 configure 中添加:
注意中間件的順序,UseRouting放在最前邊,UseAuthentication在UseAuthorization前邊,:
這樣就完成了,結(jié)果也不用看了,大家自行測(cè)試即可,無論添加或者不添加 token ,都不會(huì)報(bào)錯(cuò)。
?
?
4、補(bǔ)充:什么是 Claim
如果對(duì)?claim[] 定義不是很理解,可以看看dudu大神的解釋《理解ASP.NET Core驗(yàn)證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文》:
這篇英文博文是 Andrew Lock 寫的 Introduction to Authentication with ASP.NET Core 。
?
5、其他注意點(diǎn)
1、然后再Startup的Configure中,將TokenAuth注冊(cè)中間件
注意1:HTTP管道是有先后順序的,一定要寫在 app.Mvc() 之前,否則不起作用。
?
注意2:這里我們是自定義了認(rèn)證中間件,來對(duì)JWT的字符串進(jìn)行自定義授權(quán)認(rèn)證,所以上邊都很正常,甚至我們的Token可以不用帶 Bearer 特定字符串,如果你以后遇到了使用官方認(rèn)證中間件 UseAuthentication(),那么就必須在 configureService 中對(duì)認(rèn)證進(jìn)行配置(而且Token傳遞的時(shí)候,也必須帶上"Bearer " 這樣的特定字符串,這也就是解釋了上文,為啥要帶Bearer),這里先打個(gè)預(yù)防針,因?yàn)槲业淖钚?Github 上已經(jīng)使用了官方的認(rèn)證中間件,所以除了上邊配置的那些服務(wù)外,還需要配置 Service.AddAuthentication 和 Service.AddJwtBearer 兩個(gè)服務(wù)。
?
?如果你感覺上邊沒看懂,繼續(xù)用下邊的知識(shí)點(diǎn)來鞏固吧!
?
三、核心知識(shí)點(diǎn)梳理
?
1、Bearer認(rèn)證
HTTP提供了一套標(biāo)準(zhǔn)的身份驗(yàn)證框架:服務(wù)器可以用來針對(duì)客戶端的請(qǐng)求發(fā)送質(zhì)詢(challenge),客戶端根據(jù)質(zhì)詢提供身份驗(yàn)證憑證。質(zhì)詢與應(yīng)答的工作流程如下:服務(wù)器端向客戶端返回401(Unauthorized,未授權(quán))狀態(tài)碼,并在WWW-Authenticate頭中添加如何進(jìn)行驗(yàn)證的信息,其中至少包含有一種質(zhì)詢方式。然后客戶端可以在請(qǐng)求中添加Authorization頭進(jìn)行驗(yàn)證,其Value為身份驗(yàn)證的憑證信息。
在HTTP標(biāo)準(zhǔn)驗(yàn)證方案中,我們比較熟悉的是"Basic"和"Digest",前者將用戶名密碼使用BASE64編碼后作為驗(yàn)證憑證,后者是Basic的升級(jí)版,更加安全,因?yàn)锽asic是明文傳輸密碼信息,而Digest是加密后傳輸。在前文介紹的Cookie認(rèn)證屬于Form認(rèn)證,并不屬于HTTP標(biāo)準(zhǔn)驗(yàn)證。
本文要介紹的Bearer驗(yàn)證也屬于HTTP協(xié)議標(biāo)準(zhǔn)驗(yàn)證,它隨著OAuth協(xié)議而開始流行,詳細(xì)定義見:?RFC 6570。
A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).
Bearer驗(yàn)證中的憑證稱為BEARER_TOKEN,或者是access_token,它的頒發(fā)和驗(yàn)證完全由我們自己的應(yīng)用程序來控制,而不依賴于系統(tǒng)和Web服務(wù)器,Bearer驗(yàn)證的標(biāo)準(zhǔn)請(qǐng)求方式如下:
Authorization: Bearer [BEARER_TOKEN]那么使用Bearer驗(yàn)證有什么好處呢?
CORS: cookies + CORS 并不能跨不同的域名。而Bearer驗(yàn)證在任何域名下都可以使用HTTP header頭部來傳輸用戶信息。
對(duì)移動(dòng)端友好: 當(dāng)你在一個(gè)原生平臺(tái)(iOS, Android, WindowsPhone等)時(shí),使用Cookie驗(yàn)證并不是一個(gè)好主意,因?yàn)槟愕煤虲ookie容器打交道,而使用Bearer驗(yàn)證則簡(jiǎn)單的多。
CSRF: 因?yàn)锽earer驗(yàn)證不再依賴于cookies, 也就避免了跨站請(qǐng)求攻擊。
標(biāo)準(zhǔn):在Cookie認(rèn)證中,用戶未登錄時(shí),返回一個(gè)302到登錄頁(yè)面,這在非瀏覽器情況下很難處理,而Bearer驗(yàn)證則返回的是標(biāo)準(zhǔn)的401 challenge。
2、JWT(JSON WEB TOKEN)
上面介紹的Bearer認(rèn)證,其核心便是BEARER_TOKEN,而最流行的Token編碼方式便是:JSON WEB TOKEN。
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519)。該token被設(shè)計(jì)為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密。
JWT是由.分割的如下三部分組成:
頭部(Header)
Header 一般由兩個(gè)部分組成:
alg
typ
alg是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的類型,在這里就是:JWT。
{"alg": "HS256","typ": "JWT" }然后使用Base64Url編碼成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>載荷(Payload)
這一部分是JWT主要的信息存儲(chǔ)部分,其中包含了許多種的聲明(claims)。
Claims的實(shí)體一般包含用戶和一些元數(shù)據(jù),這些claims分成三種類型:
reserved claims:預(yù)定義的 一些聲明,并不是強(qiáng)制的但是推薦,它們包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(這里都使用三個(gè)字母的原因是保證 JWT 的緊湊)。
public claims: 公有聲明,這個(gè)部分可以隨便定義,但是要注意和 IANA JSON Web Token 沖突。
private claims: 私有聲明,這個(gè)部分是共享被認(rèn)定信息中自定義部分。
一個(gè)簡(jiǎn)單的Pyload可以是這樣子的:
{"sub": "1234567890","name": "John Doe","admin": true }這部分同樣使用Base64Url編碼成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>簽名(Signature)
Signature是用來驗(yàn)證發(fā)送者的JWT的同時(shí)也能確保在期間不被篡改。
在創(chuàng)建該部分時(shí)候你應(yīng)該已經(jīng)有了編碼后的Header和Payload,然后使用保存在服務(wù)端的秘鑰對(duì)其簽名,一個(gè)完整的JWT如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ因此使用JWT具有如下好處:
通用:因?yàn)閖son的通用性,所以JWT是可以進(jìn)行跨語(yǔ)言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語(yǔ)言都可以使用。
緊湊:JWT的構(gòu)成非常簡(jiǎn)單,字節(jié)占用很小,可以通過 GET、POST 等放在 HTTP 的 header 中,非常便于傳輸。
擴(kuò)展:JWT是自我包涵的,包含了必要的所有信息,不需要在服務(wù)端保存會(huì)話信息, 非常易于應(yīng)用的擴(kuò)展。
關(guān)于更多JWT的介紹,網(wǎng)上非常多,這里就不再多做介紹。下面,演示一下 ASP.NET Core 中 JwtBearer 認(rèn)證的使用方式。
3、示例
模擬Token
ASP.NET Core 內(nèi)置的JwtBearer驗(yàn)證,并不包含Token的發(fā)放,我們先模擬一個(gè)簡(jiǎn)單的實(shí)現(xiàn):
[HttpPost("authenticate")] public IActionResult Authenticate([FromBody]UserDto userDto) { var user = _store.FindUser(userDto.UserName, userDto.Password); if (user == null) return Unauthorized(); var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(Consts.Secret); var authTime = DateTime.UtcNow; var expiresAt = authTime.AddDays(7); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(JwtClaimTypes.Audience,"api"), new Claim(JwtClaimTypes.Issuer,"http://localhost:5200"), new Claim(JwtClaimTypes.Id, user.Id.ToString()), new Claim(JwtClaimTypes.Name, user.Name), new Claim(JwtClaimTypes.Email, user.Email), new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber) }), Expires = expiresAt, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return Ok(new { access_token = tokenString, token_type = "Bearer", profile = new { sid = user.Id, name = user.Name, auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds(), expires_at = new DateTimeOffset(expiresAt).ToUnixTimeSeconds() } }); }如上,使用微軟提供的Microsoft.IdentityModel.Tokens幫助類(源碼地址:azure-activedirectory-identitymodel-extensions-for-dotnet),可以很容易的創(chuàng)建出JwtToen,就不再多說。
注冊(cè)JwtBearer認(rèn)證
首先添加JwtBearer包引用:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0然后在Startup類中添加如下配置
在JwtBearerOptions的配置中,通常IssuerSigningKey(簽名秘鑰),?ValidIssuer(Token頒發(fā)機(jī)構(gòu)),?ValidAudience(頒發(fā)給誰(shuí))?三個(gè)參數(shù)是必須的,后兩者用于與TokenClaims中的Issuer和Audience進(jìn)行對(duì)比,不一致則驗(yàn)證失敗(與上面發(fā)放Token中的Claims對(duì)應(yīng))。
而NameClaimType和RoleClaimType需與Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes,否則會(huì)造成User.Identity.Name為空等問題。
添加受保護(hù)資源
創(chuàng)建一個(gè)需要授權(quán)的控制器,直接使用Authorize即可:
[Authorize] [Route("api/[controller]")] public class SampleDataController : Controller {[HttpGet("[action]")]public IEnumerable<WeatherForecast> WeatherForecasts(){return ...} }運(yùn)行
最后運(yùn)行,直接訪問/api/SampleData/WeatherForecasts,將返回一個(gè)401:
HTTP/1.1 401 Unauthorized Server: Kestrel Content-Length: 0 WWW-Authenticate: Bearer讓我們調(diào)用api/oauth/authenticate,獲取一個(gè)JWT:
請(qǐng)求: POST http://localhost:5200/api/oauth/authenticate HTTP/1.1 content-type: application/json{"username": "alice","password": "alice" }響應(yīng): HTTP/1.1 200 OK {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc","token_type":"Bearer","profile":{"sid":1,"name":"alice","auth_time":1509464340,"expires_at":1510069140}}最后使用該Token,再次調(diào)用受保護(hù)資源:
GET http://localhost:5200/api/SampleData/WeatherForecasts HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc授權(quán)成功,返回了預(yù)期的數(shù)據(jù):
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8[{"dateFormatted":"2017/11/3","temperatureC":35,"summary":"Chil l y","temperatureF":94}]4、擴(kuò)展
自定義Token獲取方式
JwtBearer認(rèn)證中,默認(rèn)是通過Http的Authorization頭來獲取的,這也是最推薦的做法,但是在某些場(chǎng)景下,我們可能會(huì)使用Url或者是Cookie來傳遞Token,那要怎么來實(shí)現(xiàn)呢?
其實(shí)實(shí)現(xiàn)起來非常簡(jiǎn)單,如前幾章介紹的一樣,JwtBearer也在認(rèn)證的各個(gè)階段為我們提供了事件,來執(zhí)行我們的自定義邏輯:
.AddJwtBearer(o => {o.Events = new JwtBearerEvents(){OnMessageReceived = context =>{context.Token = context.Request.Query["access_token"];return Task.CompletedTask;}};o.TokenValidationParameters = new TokenValidationParameters{...};然后在Url中添加access_token=[token],直接在瀏覽器中訪問:
同樣的,我們也可以很容易的在Cookie中讀取Token,就不再演示。
除了OnMessageReceived外,還提供了如下幾個(gè)事件:
TokenValidated:在Token驗(yàn)證通過后調(diào)用。
AuthenticationFailed: 認(rèn)證失敗時(shí)調(diào)用。
Challenge: 未授權(quán)時(shí)調(diào)用。
使用OIDC服務(wù)
在上面的示例中,我們簡(jiǎn)單模擬的Token頒發(fā),功能非常簡(jiǎn)單,并不適合在生產(chǎn)環(huán)境中使用,可是微軟也沒有提供OIDC服務(wù)的實(shí)現(xiàn),好在.NET社區(qū)中提供了幾種實(shí)現(xiàn),可供我們選擇:
| AspNet.Security.OpenIdConnect.Server (ASOS) | Low-level/protocol-first OpenID Connect server framework for ASP.NET Core and OWIN/Katana |
| IdentityServer4 | OpenID Connect and OAuth 2.0 framework for ASP.NET Core - officially certified by the OpenID Foundation and under governance of the .NET Foundation |
| OpenIddict | Easy-to-use OpenID Connect server for ASP.NET Core |
| PwdLess | Simple, stateless, passwordless authentication for ASP.NET Core |
我們?cè)谶@里使用IdentityServer4來搭建一個(gè)OIDC服務(wù)器,具體代碼會(huì)給大家?guī)砘煜?#xff0c;所以忽略了。
?
?
四、常見疑惑解析
1、JWT里會(huì)存在一些用戶的信息,比如用戶id、角色role 等等,這樣會(huì)不會(huì)不安全,信息被泄露?
答:JWT 本來就是一種無狀態(tài)的登錄授權(quán)認(rèn)證,用來替代每次請(qǐng)求都需要輸入用戶名+密碼的尷尬情況,存在一些不重要的明文很正常,只要不把隱私放出去就行,就算是被動(dòng)機(jī)不良的人得到,也做不了什么事情。
2、生成 JWT 的時(shí)候需要 secret ,但是 解密的時(shí)候 為啥沒有用到 secret ?
答:secret的作用,主要是用來防止 token 被偽造和篡改的,想想上邊的那個(gè)第一個(gè)問題,用戶得到了你的令牌,獲取到了你的個(gè)人信息,這個(gè)是沒事兒的,他什么也干不了,但是如果用戶自己隨便的生成一個(gè) token ,帶上你的uid,豈不是隨便就可以訪問資源服務(wù)器了,所以這個(gè)時(shí)候就需要一個(gè) secret 來生成 token,這樣的話,就能保證數(shù)字簽名的正確性。
而且,在我們資源服務(wù)器里,將token解析的時(shí)候,微軟封裝了方法,將secret進(jìn)行校驗(yàn)了,這就是保證了token的安全性,從而保證我們的資源api是安全的,你不信的話,可以用你網(wǎng)站的 token 來訪問我的在線項(xiàng)目,就算是 uid,role等等全部正確,還是不能訪問我的網(wǎng)站,因?yàn)槟悴恢牢业膕ecret,所以你生成的令牌對(duì)我的是無效的。
?
可以看看這個(gè)視頻:https://www.bilibili.com/video/av52076900?share_medium=android&share_source=qq&bbid=XZ786B57591674D68847894D8F16996AAFFB6&ts=1559452290064
?
?
?
五、結(jié)語(yǔ)
好啦!項(xiàng)目準(zhǔn)備階段就這么結(jié)束了,以后咱們就可以直接用swagger來調(diào)試了,而不是每次都用F5運(yùn)行等,接下來我們就要正式開始搭建項(xiàng)目了,主要采用的是泛型倉(cāng)儲(chǔ)模式 Repository+Service,也是一種常見的模式。
六、Github
本系列開源地址
https://github.com/anjoy8/Blog.Core.git
本文章小Demo
https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_JWT
?
一起學(xué)習(xí),一起進(jìn)步 ? QQ群:867095512
總結(jié)
以上是生活随笔為你收集整理的【 .NET Core 3.0 】框架之五 || JWT权限验证的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何摆脱「技术思维」的惯性?
- 下一篇: asp.net core 3.0 更新简