【 .NET Core 3.0 】框架之五 || JWT权限验证
前言
關于JWT一共三篇 姊妹篇,內容分別從簡單到復雜,一定要多看多想:
? ? ? 一、Swagger的使用 3.3 JWT權限驗證【修改】
? ? ? 二、解決JWT權限驗證過期問題
? ? ? 三、JWT完美實現權限與接口的動態(tài)分配
?這里一共三個文章,目前是第一篇,剩下兩篇主要是在博客園,大家點擊閱讀原文,自行查看就行。
本文有配套視頻:https://www.bilibili.com/video/av58096866/?p=4
1、如何給接口實現權限驗證?
其實關于這一塊,我思考了下,因為畢竟我的項目中是使用的vue + api 搭建一個前臺展示,大部分頁面都沒有涉及到權限驗證,本來要忽略這一章節(jié),可是猶豫再三,還是給大家簡單分析了下,個人還是希望陪大家一直搭建一個較為強大的,只要是涉及到后端那一定就需要?登錄=》驗證了
根據維基百科定義,JWT(讀作 [/d??t/]),即JSON Web Tokens,是一種基于JSON的、用于在網絡上聲明某種主張的令牌(token)。JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。它是一種用于雙方之間傳遞安全信息的表述性聲明規(guī)范。JWT作為一個開放的標準(RFC 7519),定義了一種簡潔的、自包含的方法,從而使通信雙方實現以JSON對象的形式安全的傳遞信息。
以上是JWT的官方解釋,可以看出JWT并不是一種只能權限驗證的工具,而是一種標準化的數據傳輸規(guī)范。所以,只要是在系統(tǒng)之間需要傳輸簡短但卻需要一定安全等級的數據時,都可以使用JWT規(guī)范來傳輸。規(guī)范是不因平臺而受限制的,這也是JWT做為授權驗證可以跨平臺的原因。
如果理解還是有困難的話,我們可以拿JWT和JSON類比:
JSON是一種輕量級的數據交換格式,是一種數據層次結構規(guī)范。它并不是只用來給接口傳遞數據的工具,只要有層級結構的數據都可以使用JSON來存儲和表示。當然,JSON也是跨平臺的,不管是Win還是Linux,.NET還是Java,都可以使用它作為數據傳輸形式。
1)客戶端向授權服務系統(tǒng)發(fā)起請求,申請獲取“令牌”。
2)授權服務根據用戶身份,生成一張專屬“令牌”,并將該“令牌”以JWT規(guī)范返回給客戶端
3)客戶端將獲取到的“令牌”放到http請求的headers中后,向主服務系統(tǒng)發(fā)起請求。主服務系統(tǒng)收到請求后會從headers中獲取“令牌”,并從“令牌”中解析出該用戶的身份權限,然后做出相應的處理(同意或拒絕返回資源)
?
?
零、生成 Token 令牌
關于JWT授權,其實過程是很簡單的,大家其實這個時候靜下心想一想就能明白,這個就是四步走:
首先我們需要一個具有一定規(guī)則的 Token 令牌,也就是 JWT 令牌(比如我們的公司門禁卡),//登錄
然后呢,我們再定義哪些地方需要什么樣的角色(比如領導辦公室我們是沒辦法進去的),//授權機制
接下來,整個公司需要定一個規(guī)則,就是如何對這個 Token 進行驗證,不能隨便寫個字條,這樣容易被造假(比如我們公司門上的每一道刷卡機),//認證方案
最后,就是安全部門,開啟認證中間件服務(那這個服務可以關閉的,比如我們電影里看到的黑客會把這個服務給關掉,這樣整個公司安保就形同虛設了)。//開啟中間件
?
那現在我們就是需要一個具有一定規(guī)則的 Token 令牌,大家可以參考 JwtHelper 這個類:
這個實體類就是用來生成 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 中,如果你想知道如何在其他地方將這個 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()}") , //這個就是過期時間,目前是過期1000秒,可自定義,注意JWT有自己的緩沖過期時間 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),//為了解決一個用戶多個角色(比如: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); 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; } }這里邊有一個 Appsettings 類,主要的作用是自動讀取項目配置文件 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]; } }?
?
這個接口如何調用呢,很簡單,就是我們的登錄api:
public async Task<object> GetJwtStr(string name, string pass) { string jwtStr = string.Empty; bool suc = false; // 獲取用戶的角色名,請暫時忽略其內部是如何獲取的,可以直接用 var userRole="Admin"; 來代替更好理解。 var userRole = await _sysUserInfoServices.GetUserRoleNameStr(name, pass); if (userRole != null) { // 將用戶id和角色名,作為單獨的自定義變量封裝進 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 }); }?
現在我們獲取到Token了,那如何進行授權認證呢,別著急,重頭戲馬上到來!
?
一、JWT授權認證流程——自定義中間件
在之前的搭建中,swagger已經基本成型,其實其功能之多,不是我這三篇所能寫完的,想要添加權限,先從服務開始
0、Swagger中開啟JWT服務
我們要測試 JWT 授權認證,就必定要輸入 Token令牌,那怎么輸入呢,平時的話,我們可以使用 Postman 來控制輸入,就是在請求的時候,在 Header 中,添加Authorization屬性,
但是我們現在使用了 Swagger 作為接口文檔,那怎么輸入呢,別著急, Swagger 已經幫我們實現了這個錄入 Token令牌的功能:
在startup.cs 中的 ConfigureServices? ->?AddSwaggerGen?服務中,增加以下代碼,注意是swagger服務內部:
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");//這個就是剛剛配置的xml文件名 c.IncludeXmlComments(xmlPath, true);//默認的第二個參數是false,這個是controller的注釋,記得修改 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//這個就是Model層的xml文件名 c.IncludeXmlComments(xmlModelPath); c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>(); #region Token綁定到ConfigureServices c.AddSecurityDefinition("oauth2",?new?OpenApiSecurityScheme { Description = "JWT授權(數據將在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間是一個空格)\"", Name = "Authorization",//jwt默認的參數名稱 In = ParameterLocation.Header,//jwt默認存放Authorization信息的位置(請求頭中) Type = SecuritySchemeType.ApiKey }); #endregion });
?
然后執(zhí)行代碼,就可以在 swagger/index.html 頁面里看到這個Token入口了:
?
?
大家點開,看到輸入框,在輸入Token的時候,需要在Token令牌的前邊加上Bearer (為什么要加這個,下文會說明,請一定要注意看,一定要明白為啥要帶,因為它涉及到了什么是授權,什么是認證,還要自定義認證中間件還是官方認證中間件的區(qū)別,請注意看下文),比如是這樣的:
但是請注意!如果你使用的是中間件 app.UseMiddleware<JwtTokenAuth>(),或者 app.UseJwtTokenAuth() 的時候(兩種寫法一樣) ,要是使用 Bearer xxxx傳值的時候,記得在中間件的方法中,把Token的 “Bearer 空格” 字符給截取掉,這樣的:
?
1:API接口授權策略
這里可以直接在api接口上,直接設置該接口所對應的角色權限信息:
這個時候我們就需要對每一個接口設置對應的 Roles 信息,但是如果我們的接口需要對應多個角色的時候,我們就可以直接寫多個:
?
這里有一個情況,如果角色多的話,不僅不利于我們閱讀,還可能在配置的時候少一兩個role,比如這個 api接口1 少了一個 system 的角色,再比如那個 api接口2 把 Admin 角色寫成了 Adnin 這種不必要的錯誤,真是很難受,那怎么辦呢,欸!這個時候就出現了基于策略的授權機制:
我們在 ConfigureService 中可以這么設置:
// 1【授權】、這個和上邊的異曲同工,好處就是不用在controller中,寫多個 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ù)走第二步,身份驗證方案。
?關于授權認證有兩種方式,可以使用官方的認證方式,也可以使用自定義中間件的方法,具體請往下看,咱們先說說如何進行自定義認證。
?
2、自定義認證之身份驗證設置
上邊第一步中,咱們已經對每一個接口api設置好了 授權機制 ,那這里就要開始認證,咱們先看看如何實現自定義的認證:
?
JwtTokenAuth,一個中間件,用來過濾每一個http請求,就是每當一個用戶發(fā)送請求的時候,都先走這一步,然后再去訪問http請求的接口
?
?
?
?
前兩步咱們都完成了,從授權到自定義身份驗證方案,就剩下最后一步,開啟中間件了。
?
3:開啟自定義認證中間件,實現Http信道攔截
這個很簡單,只需要在 startup.cs ->?Configure 中配置認證中間件
?
4:開始測試
這個時候我們的自定義JWT授權認證已經結束了,我們開始測試,假設對某一個 api接口設置了權限:
?
在我們沒有輸入 Token 的時候,點擊測試接口會報錯:
?
?
這個錯誤很明顯,就是說我們沒有配置默認的認證方案,也沒有自定義身份驗證方案,
但是這個時候我們再進行試驗:
剛剛上邊的情況是我們沒有輸入 Token ,但是如果我們輸入token呢?看看是不是又會報錯?
?
?
我們發(fā)現了什么?!!沒有報錯,這是因為什么?欸,聰明的你應該想到了,請往下看,什么是 聲明主體 ClaimsPrincipal 。
?
5、聲明主體 ClaimsPrincipal 是如何保存的?
在上邊,我們解決了一些問題,同時也出現了一個問題,就是為什么不輸入 Token 就報錯了,而輸入了 Bearer xxxxxxxxxxx 這樣的Token 就不報錯了呢?這里要說到 聲明主體的作用了。
就是我們上邊寫的自定義中間件,大家可以再來看看:
?
?這個時候你就應該明白了吧,
1、首先我們自定義授權認證,為啥可以不用進行下邊截圖中官方認證那一塊的配置:
?
?
因為這一塊官方的服務,就等同于我們的自定義身份驗證方案——中間件。
2、你應該明白,為什么不輸入token的時候報錯,而輸入了就不報錯了?
因為沒有輸入的時候,直接 return了,并沒有在 httpContext 上下文中,進行配置聲明主體?httpContext.User = principal 。
所以說,我們無論是自定義中間件的自定義身份驗證方案,還是官方的認證方案,只要我們的登錄了,也就是說,只要我們實現了某種規(guī)則:
?
這樣,就會觸發(fā)我們的內部服務,將當前 token 所攜帶的信息,進行自動解碼,然后填充到聲明主體里(自定義中間件需要手動配置,官方的自動就實現該操作),
所以這個時候我們就可以輕松的拿到想到的東西,比如這里這些:
?
?
6、無策略依然授權錯誤
上邊咱們說到了,如果我們自定義中間件的話,在中間件中,我們在 Claims 添加了角色的相關權限:
而且很自然的在 接口中,也是分為兩種情況:要么沒有加權限,要么就是基于角色的加權:
?
?但是如果這個時候,我們直接對接口增加 無任何策略 的加權:
?
就是沒有任何的策略,我們登錄,然后添加 token,一看,還是報錯了!
本來 [Authorize] 這種 無策略 的授權,按理說只需要我們登錄了就可以了,不需要其他任何限制就可以訪問,但是現在依然報錯401 ,證明我們的中間件并不能對這種方案起到效果,你可能會問,那帶有 Roles=“Admin” 的為啥可以呢?反而這種無策略的不行呢,我個人感覺可能還是中間件咱們設計的解決方案就是基于角色授權的那種,(我也再研究研究,看看能不能完善下這個自定義中間件,使它能適應這個 無具體策略 的加權方案,但是可能寫到最后,就是無限接近官方的授權中間件了哈哈)。
這個時候我們發(fā)現,自定義中間件還是挺麻煩的,但是你通過自己使用自定義授權中間件,不僅僅可以了解到中間件的使用,還可以了解 netcore 到底是如何授權的機制,但是我還是建議大家使用官方的認證方案,畢竟他們考慮的很全面的。
?
那么如果我們想要用官方的認證方案呢,要怎么寫呢?請往下看:
?
二、JWT授權認證流程——官方認證
上邊咱們說完了自定義中間件的形式,發(fā)現了也方便的地方,也有不方便之處,雖然靈活的使用了自定義身份驗證,但是畢竟很受限,而且也無法對過期時間進行判斷,以后的文章你會看到《36 ║解決JWT自定義中間件授權過期問題》,這里先不說,重點說說,如何通過官方認證來實現。
1:API接口授權策略
和上邊自定義的過程一模一樣,略。
?
2、官方默認認證配置
在剛剛上邊,咱們說到了一個錯誤,不知道還有沒有印象:
No authenticationScheme was specified, and there was no DefaultChallengeScheme found.?
就是這個,自定義認證中間件呢,就是前者,那官方的,就是后者 DefaultChallengeScheme;
?
很簡單,只需要在 configureService 中,添加【統(tǒng)一認證】即可:
#region 【第二步:配置認證服務】 // 令牌驗證參數 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【認證】、core自帶官方JWT認證 // 開啟Bearer認證 services.AddAuthentication("Bearer") // 添加JwtBearer服務 .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; } }; });上邊代碼中出現的部分參數定義(如果還看不懂,請看項目代碼):
//讀取配置文件 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);?
具體的每個配置的含義呢,我的代碼里都有,大家自己可以看看,都很簡單。
劃重點:我們就是用的這個官方默認的方案,來替換了我們自定義中間件的身份驗證方案,從而達到目的,說白了,就是官方封裝了一套方案,這樣我們就不用寫中間件了。
?
3、配置官方認證中間件
這個很簡單,還是在 configure 中添加:
注意中間件的順序,UseRouting放在最前邊,UseAuthentication在UseAuthorization前邊,:
這樣就完成了,結果也不用看了,大家自行測試即可,無論添加或者不添加 token ,都不會報錯。
?
?
4、補充:什么是 Claim
如果對?claim[] 定義不是很理解,可以看看dudu大神的解釋《理解ASP.NET Core驗證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文》:
這篇英文博文是 Andrew Lock 寫的 Introduction to Authentication with ASP.NET Core 。
?
5、其他注意點
1、然后再Startup的Configure中,將TokenAuth注冊中間件
注意1:HTTP管道是有先后順序的,一定要寫在 app.Mvc() 之前,否則不起作用。
?
注意2:這里我們是自定義了認證中間件,來對JWT的字符串進行自定義授權認證,所以上邊都很正常,甚至我們的Token可以不用帶 Bearer 特定字符串,如果你以后遇到了使用官方認證中間件 UseAuthentication(),那么就必須在 configureService 中對認證進行配置(而且Token傳遞的時候,也必須帶上"Bearer " 這樣的特定字符串,這也就是解釋了上文,為啥要帶Bearer),這里先打個預防針,因為我的最新 Github 上已經使用了官方的認證中間件,所以除了上邊配置的那些服務外,還需要配置 Service.AddAuthentication 和 Service.AddJwtBearer 兩個服務。
?
?如果你感覺上邊沒看懂,繼續(xù)用下邊的知識點來鞏固吧!
?
三、核心知識點梳理
?
1、Bearer認證
HTTP提供了一套標準的身份驗證框架:服務器可以用來針對客戶端的請求發(fā)送質詢(challenge),客戶端根據質詢提供身份驗證憑證。質詢與應答的工作流程如下:服務器端向客戶端返回401(Unauthorized,未授權)狀態(tài)碼,并在WWW-Authenticate頭中添加如何進行驗證的信息,其中至少包含有一種質詢方式。然后客戶端可以在請求中添加Authorization頭進行驗證,其Value為身份驗證的憑證信息。
在HTTP標準驗證方案中,我們比較熟悉的是"Basic"和"Digest",前者將用戶名密碼使用BASE64編碼后作為驗證憑證,后者是Basic的升級版,更加安全,因為Basic是明文傳輸密碼信息,而Digest是加密后傳輸。在前文介紹的Cookie認證屬于Form認證,并不屬于HTTP標準驗證。
本文要介紹的Bearer驗證也屬于HTTP協(xié)議標準驗證,它隨著OAuth協(xié)議而開始流行,詳細定義見:?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驗證中的憑證稱為BEARER_TOKEN,或者是access_token,它的頒發(fā)和驗證完全由我們自己的應用程序來控制,而不依賴于系統(tǒng)和Web服務器,Bearer驗證的標準請求方式如下:
Authorization: Bearer [BEARER_TOKEN]那么使用Bearer驗證有什么好處呢?
CORS: cookies + CORS 并不能跨不同的域名。而Bearer驗證在任何域名下都可以使用HTTP header頭部來傳輸用戶信息。
對移動端友好: 當你在一個原生平臺(iOS, Android, WindowsPhone等)時,使用Cookie驗證并不是一個好主意,因為你得和Cookie容器打交道,而使用Bearer驗證則簡單的多。
CSRF: 因為Bearer驗證不再依賴于cookies, 也就避免了跨站請求攻擊。
標準:在Cookie認證中,用戶未登錄時,返回一個302到登錄頁面,這在非瀏覽器情況下很難處理,而Bearer驗證則返回的是標準的401 challenge。
2、JWT(JSON WEB TOKEN)
上面介紹的Bearer認證,其核心便是BEARER_TOKEN,而最流行的Token編碼方式便是:JSON WEB TOKEN。
Json web token (JWT), 是為了在網絡應用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準(RFC 7519)。該token被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其它業(yè)務邏輯所必須的聲明信息,該token也可直接被用于認證,也可被加密。
JWT是由.分割的如下三部分組成:
頭部(Header)
Header 一般由兩個部分組成:
alg
typ
alg是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的類型,在這里就是:JWT。
{"alg": "HS256","typ": "JWT" }然后使用Base64Url編碼成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>載荷(Payload)
這一部分是JWT主要的信息存儲部分,其中包含了許多種的聲明(claims)。
Claims的實體一般包含用戶和一些元數據,這些claims分成三種類型:
reserved claims:預定義的 一些聲明,并不是強制的但是推薦,它們包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(這里都使用三個字母的原因是保證 JWT 的緊湊)。
public claims: 公有聲明,這個部分可以隨便定義,但是要注意和 IANA JSON Web Token 沖突。
private claims: 私有聲明,這個部分是共享被認定信息中自定義部分。
一個簡單的Pyload可以是這樣子的:
{"sub": "1234567890","name": "John Doe","admin": true }這部分同樣使用Base64Url編碼成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>簽名(Signature)
Signature是用來驗證發(fā)送者的JWT的同時也能確保在期間不被篡改。
在創(chuàng)建該部分時候你應該已經有了編碼后的Header和Payload,然后使用保存在服務端的秘鑰對其簽名,一個完整的JWT如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ因此使用JWT具有如下好處:
通用:因為json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
緊湊:JWT的構成非常簡單,字節(jié)占用很小,可以通過 GET、POST 等放在 HTTP 的 header 中,非常便于傳輸。
擴展:JWT是自我包涵的,包含了必要的所有信息,不需要在服務端保存會話信息, 非常易于應用的擴展。
關于更多JWT的介紹,網上非常多,這里就不再多做介紹。下面,演示一下 ASP.NET Core 中 JwtBearer 認證的使用方式。
3、示例
模擬Token
ASP.NET Core 內置的JwtBearer驗證,并不包含Token的發(fā)放,我們先模擬一個簡單的實現:
[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,就不再多說。
注冊JwtBearer認證
首先添加JwtBearer包引用:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0然后在Startup類中添加如下配置
在JwtBearerOptions的配置中,通常IssuerSigningKey(簽名秘鑰),?ValidIssuer(Token頒發(fā)機構),?ValidAudience(頒發(fā)給誰)?三個參數是必須的,后兩者用于與TokenClaims中的Issuer和Audience進行對比,不一致則驗證失敗(與上面發(fā)放Token中的Claims對應)。
而NameClaimType和RoleClaimType需與Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes,否則會造成User.Identity.Name為空等問題。
添加受保護資源
創(chuàng)建一個需要授權的控制器,直接使用Authorize即可:
[Authorize] [Route("api/[controller]")] public class SampleDataController : Controller {[HttpGet("[action]")]public IEnumerable<WeatherForecast> WeatherForecasts(){return ...} }運行
最后運行,直接訪問/api/SampleData/WeatherForecasts,將返回一個401:
HTTP/1.1 401 Unauthorized Server: Kestrel Content-Length: 0 WWW-Authenticate: Bearer讓我們調用api/oauth/authenticate,獲取一個JWT:
請求: POST http://localhost:5200/api/oauth/authenticate HTTP/1.1 content-type: application/json{"username": "alice","password": "alice" }響應: 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,再次調用受保護資源:
GET http://localhost:5200/api/SampleData/WeatherForecasts HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.Y1TDz8KjLRh_vjQ_3iYP4oJw-fmhoboiAGPqIZ-ooNc授權成功,返回了預期的數據:
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、擴展
自定義Token獲取方式
JwtBearer認證中,默認是通過Http的Authorization頭來獲取的,這也是最推薦的做法,但是在某些場景下,我們可能會使用Url或者是Cookie來傳遞Token,那要怎么來實現呢?
其實實現起來非常簡單,如前幾章介紹的一樣,JwtBearer也在認證的各個階段為我們提供了事件,來執(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外,還提供了如下幾個事件:
TokenValidated:在Token驗證通過后調用。
AuthenticationFailed: 認證失敗時調用。
Challenge: 未授權時調用。
使用OIDC服務
在上面的示例中,我們簡單模擬的Token頒發(fā),功能非常簡單,并不適合在生產環(huán)境中使用,可是微軟也沒有提供OIDC服務的實現,好在.NET社區(qū)中提供了幾種實現,可供我們選擇:
| 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 |
我們在這里使用IdentityServer4來搭建一個OIDC服務器,具體代碼會給大家?guī)砘煜?#xff0c;所以忽略了。
?
?
四、常見疑惑解析
1、JWT里會存在一些用戶的信息,比如用戶id、角色role 等等,這樣會不會不安全,信息被泄露?
答:JWT 本來就是一種無狀態(tài)的登錄授權認證,用來替代每次請求都需要輸入用戶名+密碼的尷尬情況,存在一些不重要的明文很正常,只要不把隱私放出去就行,就算是被動機不良的人得到,也做不了什么事情。
2、生成 JWT 的時候需要 secret ,但是 解密的時候 為啥沒有用到 secret ?
答:secret的作用,主要是用來防止 token 被偽造和篡改的,想想上邊的那個第一個問題,用戶得到了你的令牌,獲取到了你的個人信息,這個是沒事兒的,他什么也干不了,但是如果用戶自己隨便的生成一個 token ,帶上你的uid,豈不是隨便就可以訪問資源服務器了,所以這個時候就需要一個 secret 來生成 token,這樣的話,就能保證數字簽名的正確性。
而且,在我們資源服務器里,將token解析的時候,微軟封裝了方法,將secret進行校驗了,這就是保證了token的安全性,從而保證我們的資源api是安全的,你不信的話,可以用你網站的 token 來訪問我的在線項目,就算是 uid,role等等全部正確,還是不能訪問我的網站,因為你不知道我的secret,所以你生成的令牌對我的是無效的。
?
可以看看這個視頻:https://www.bilibili.com/video/av52076900?share_medium=android&share_source=qq&bbid=XZ786B57591674D68847894D8F16996AAFFB6&ts=1559452290064
?
?
?
五、結語
好啦!項目準備階段就這么結束了,以后咱們就可以直接用swagger來調試了,而不是每次都用F5運行等,接下來我們就要正式開始搭建項目了,主要采用的是泛型倉儲模式 Repository+Service,也是一種常見的模式。
六、Github
本系列開源地址
https://github.com/anjoy8/Blog.Core.git
本文章小Demo
https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_JWT
?
一起學習,一起進步 ? QQ群:867095512
總結
以上是生活随笔為你收集整理的【 .NET Core 3.0 】框架之五 || JWT权限验证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何摆脱「技术思维」的惯性?
- 下一篇: asp.net ajax控件工具集 Au