https://mp.weixin.qq.com/s/7135y3MkUlPIp-flfwscightml
關於JWT一共三篇 姊妹篇,內容分別從簡單到複雜,必定要多看多想:vue
1、Swagger的使用 3.3 JWT權限驗證【修改】android
2、解決JWT權限驗證過時問題git
3、JWT完美實現權限與接口的動態分配github
本文章不只在Blog.Core 框架裏有代碼,並且我也單寫了一個關於 JWT 的小demo,在文章末,你們能夠下載看看。web
書接上文,在前邊的兩篇文章中,咱們簡單提到了接口文檔神器Swagger,算法
《三 || Swagger的使用 3.1》、json
《四 || Swagger的使用 3.2》,後端
兩個文章中,也對常見的幾個問題作了簡單的討論,最後還剩下一個小問題,api
其實關於這一塊,我思考了下,由於畢竟個人項目中是使用的vue + api 搭建一個前臺展現,大部分頁面都沒有涉及到權限驗證,原本要忽略這一章節,但是猶豫再三,仍是給你們簡單分析了下,我的仍是但願陪你們一直搭建一個較爲強大的,只要是涉及到後端那必定就須要 登陸=》驗證了,本文主要是參考網友https://www.cnblogs.com/RayWang/p/9255093.html的思路,我本身稍加改動,你們均可以看看。
根據維基百科定義,JWT(讀做 [/dʒɒt/]),即JSON Web Tokens,是一種基於JSON的、用於在網絡上聲明某種主張的令牌(token)。JWT一般由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。它是一種用於雙方之間傳遞安全信息的表述性聲明規範。JWT做爲一個開放的標準(RFC 7519),定義了一種簡潔的、自包含的方法,從而使通訊雙方實現以JSON對象的形式安全的傳遞信息。
以上是JWT的官方解釋,能夠看出JWT並非一種只能權限驗證的工具,而是一種標準化的數據傳輸規範。因此,只要是在系統之間須要傳輸簡短但卻須要必定安全等級的數據時,均可以使用JWT規範來傳輸。規範是不因平臺而受限制的,這也是JWT作爲受權驗證能夠跨平臺的緣由。
若是理解仍是有困難的話,咱們能夠拿JWT和JSON類比:
JSON是一種輕量級的數據交換格式,是一種數據層次結構規範。它並非只用來給接口傳遞數據的工具,只要有層級結構的數據均可以使用JSON來存儲和表示。固然,JSON也是跨平臺的,無論是Win仍是Linux,.NET仍是Java,均可以使用它做爲數據傳輸形式。
1)客戶端向受權服務系統發起請求,申請獲取「令牌」。
2)受權服務根據用戶身份,生成一張專屬「令牌」,並將該「令牌」以JWT規範返回給客戶端
3)客戶端將獲取到的「令牌」放到http請求的headers中後,向主服務系統發起請求。主服務系統收到請求後會從headers中獲取「令牌」,並從「令牌」中解析出該用戶的身份權限,而後作出相應的處理(贊成或拒絕返回資源)
關於JWT受權,其實過程是很簡單的,你們其實這個時候靜下心想想就能明白,這個就是四步走:
首先咱們須要一個具備必定規則的 Token 令牌,也就是 JWT 令牌(好比咱們的公司門禁卡),//登陸
而後呢,咱們再定義哪些地方須要什麼樣的角色(好比領導辦公室咱們是沒辦法進去的),//受權機制
接下來,整個公司須要定一個規則,就是如何對這個 Token 進行驗證,不能隨便寫個字條,這樣容易被造假(好比咱們公司門上的每一道刷卡機),//認證方案
最後,就是安所有門,開啓認證中間件服務(那這個服務能夠關閉的,好比咱們電影裏看到的黑客會把這個服務給關掉,這樣整個公司安保就形同虛設了)。//開啓中間件
那如今咱們就是須要一個具備必定規則的 Token 令牌,你們能夠參考:
這個實體類就是用來生成 Token 的,代碼記錄以下:
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 Claim[] //old var claims = new List<Claim> { /* * 特別重要: 一、這裏將用戶的部分信息,好比 uid 存到了Claim 中,若是你想知道如何在其餘地方將這個 uid從 Token 中取出來,請看下邊的SerializeJwt() 方法,或者在整個解決方案,搜索這個方法,看哪裏使用了! 二、你也能夠研究下 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(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> /// <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; } }
public class Appsettings { static IConfiguration Configuration { get; set; } //static Appsettings() //{ // //ReloadOnChange = true 當appsettings.json被修改時從新加載 // Configuration = new ConfigurationBuilder() // .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true })//請注意要把當前appsetting.json 文件->右鍵->屬性->複製到輸出目錄->始終複製 // .Build(); //} static Appsettings() { string Path = "appsettings.json"; { //若是你把配置文件 是 根據環境變量來分開了,能夠這樣寫 //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json"; } //Configuration = new ConfigurationBuilder() //.Add(new JsonConfigurationSource { Path = Path, ReloadOnChange = true })//請注意要把當前appsetting.json 文件->右鍵->屬性->複製到輸出目錄->始終複製 //.Build(); Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//這樣的話,能夠直接讀目錄裏的json文件,而不是 bin 文件夾下的,因此不用修改複製屬性 .Build(); } /// <summary> /// 封裝要操做的字符 /// </summary> /// <param name="sections"></param> /// <returns></returns> public static string app(params string[] sections) { try { var val = string.Empty; for (int i = 0; i < sections.Length; i++) { val += sections[i] + ":"; } return Configuration[val.TrimEnd(':')]; } catch (Exception) { return ""; } } }
這個接口如何調用呢,很簡單,就是咱們的登陸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);//登陸,獲取到必定規則的 Token 令牌 suc = true; } else { jwtStr = "login fail!!!"; } return Ok(new { success = suc, token = jwtStr }); }
/// <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; } }
如今咱們獲取到Token了,那如何進行受權認證呢,彆着急,重頭戲立刻到來!
在以前的搭建中,swagger已經基本成型,其實其功能之多,不是我這三篇所能寫完的,想要添加權限,先從服務開始
咱們要測試 JWT 受權認證,就一定要輸入 Token令牌,那怎麼輸入呢,平時的話,咱們可使用 Postman 來控制輸入,就是在請求的時候,在 Header 中,添加Authorization屬性,
可是咱們如今使用了 Swagger 做爲接口文檔,那怎麼輸入呢,彆着急, Swagger 已經幫咱們實現了這個錄入 Token令牌的功能:
在ConfigureServices -> AddSwaggerGen 服務中,增長如下紅色代碼,注意是swagger服務內部:
/// <summary> /// ConfigureServices 方法 /// </summary> /// <param name="services"></param> public void ConfigureServices(IServiceCollection services) { services.AddMvc(); #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架說明文檔", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是這裏 #region 讀取xml信息 var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//這個就是剛剛配置的xml文件名 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//這個就是Model層的xml文件名 c.IncludeXmlComments(xmlPath, true);//默認的第二個參數是false,這個是controller的註釋,記得修改 c.IncludeXmlComments(xmlModelPath); #endregion #region Token綁定到ConfigureServices //添加header驗證信息 //c.OperationFilter<SwaggerHeader>(); var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, }; c.AddSecurityRequirement(security); //方案名稱「Blog.Core」可自定義,上下一致便可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT受權(數據將在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意二者之間是一個空格)\"", Name = "Authorization",//jwt默認的參數名稱 In = "header",//jwt默認存放Authorization信息的位置(請求頭中) Type = "apiKey" }); #endregion }); #endregion }
而後執行代碼,就能夠在 swagger/index.html 頁面裏看到這個Token入口了:
你們點開,看到輸入框,在輸入Token的時候,須要在Token令牌的前邊加上Bearer (爲何要加這個,下文會說明,請必定要注意看,必定要明白爲啥要帶,由於它涉及到了什麼是受權,什麼是認證,還要自定義認證中間件仍是官方認證中間件的區別,請注意看下文),好比是這樣的:
可是請注意!若是你使用的是中間件 app.UseMiddleware<JwtTokenAuth>() ,要是使用 Bearer xxxx傳值的時候,記得在中間件的方法中,把Token的 「Bearer 空格」 字符給截取掉,這樣的:
這裏能夠直接在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" }; }
這樣咱們的第一步就完成了。繼續走第二步,身份驗證方案。
關於受權認證有兩種方式,可使用官方的認證方式,也可使用自定義中間件的方法,具體請往下看,我們先說說如何進行自定義認證。
上邊第一步中,我們已經對每個接口api設置好了 受權機制 ,那這裏就要開始認證,我們先看看如何實現自定義的認證:
JwtTokenAuth,一箇中間件,用來過濾每個http請求,就是每當一個用戶發送請求的時候,都先走這一步,而後再去訪問http請求的接口
public class JwtTokenAuth { // 中間件必定要有一個next,將管道能夠正常的走下去 private readonly RequestDelegate _next; public JwtTokenAuth(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext httpContext) { //檢測是否包含'Authorization'請求頭 if (!httpContext.Request.Headers.ContainsKey("Authorization")) { return _next(httpContext); } var tokenHeader = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); try { if (tokenHeader.Length >= 128) { TokenModelJwt tm = JwtHelper.SerializeJwt(tokenHeader); //受權 Claim 關鍵 var claimList = new List<Claim>(); var claim = new Claim(ClaimTypes.Role, tm.Role); claimList.Add(claim); var identity = new ClaimsIdentity(claimList); var principal = new ClaimsPrincipal(identity); httpContext.User = principal; } } catch (Exception e) { Console.WriteLine($"{DateTime.Now} middleware wrong:{e.Message}"); } return _next(httpContext); } } // 這裏定義一箇中間件Helper,主要做用就是給當前模塊的中間件取一個別名 public static class MiddlewareHelpers { public static IApplicationBuilder UseJwtTokenAuth(this IApplicationBuilder app) { return app.UseMiddleware<JwtTokenAuth>(); } }
//自定義認證中間件 app.UseJwtTokenAuth(); //也能夠app.UseMiddleware<JwtTokenAuth>();
在咱們沒有輸入 Token 的時候,點擊測試接口會報錯:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
//沒有指定身份驗證方案, 也沒有發現默認挑戰方案。
這個錯誤很明顯,就是說咱們沒有配置默認的認證方案,也沒有自定義身份驗證方案,
可是這個時候咱們再進行試驗:
剛剛上邊的狀況是咱們沒有輸入 Token ,可是若是咱們輸入token呢?看看是否是又會報錯?
咱們發現了什麼?!!沒有報錯,這是由於什麼?欸,聰明的你應該想到了,請往下看,什麼是 聲明主體 ClaimsPrincipal 。
在上邊,咱們解決了一些問題,同時也出現了一個問題,就是爲何不輸入 Token 就報錯了,而輸入了 Bearer xxxxxxxxxxx 這樣的Token 就不報錯了呢?這裏要說到 聲明主體的做用了。
就是咱們上邊寫的自定義中間件,你們能夠再來看看:
// 自定義認證中間件,咱們省略部分代碼,來分析分析 public Task Invoke(HttpContext httpContext) { //檢測是否包含'Authorization'請求頭 if (!httpContext.Request.Headers.ContainsKey("Authorization")) { //直接返回了 http 信道 ,就出現了咱們上邊的報錯,沒有指定身份驗證方案, 也沒有發現默認挑戰方案。 return _next(httpContext); } //可是!請注意,這個時候咱們輸入了 token,咱們就會在 httpcontext 上下文中,添加上咱們本身自定義的身份驗證方案!!!這就是沒有繼續報錯的根本緣由 var tokenHeader = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); //........ //受權 var claimList = new List<Claim>(); var claim = new Claim(ClaimTypes.Role, tm.Role); claimList.Add(claim); var identity = new ClaimsIdentity(claimList); var principal = new ClaimsPrincipal(identity); httpContext.User = principal; } return _next(httpContext); }
這個時候你就應該明白了吧,
一、首先咱們自定義受權認證,爲啥能夠不用進行下邊截圖中官方認證那一塊的配置:
由於這一塊官方的服務,就等同於咱們的自定義身份驗證方案——中間件。
二、你應該明白,爲何不輸入token的時候報錯,而輸入了就不報錯了?
由於沒有輸入的時候,直接 return了,並無在 httpContext 上下文中,進行配置聲明主體 httpContext.User = principal 。
因此說,咱們不管是自定義中間件的自定義身份驗證方案,仍是官方的認證方案,只要咱們的登陸了,也就是說,只要咱們實現了某種規則:
在 Http 的 Header 裏,增長屬性Authorization ,並賦值 :Bearer xxxxxxxxxxxxxx;
這樣,就會觸發咱們的內部服務,將當前 token 所攜帶的信息,進行自動解碼,而後填充到聲明主體裏(自定義中間件須要手動配置,官方的自動就實現該操做),
因此這個時候咱們就能夠輕鬆的拿到想到的東西,好比這裏這些:
上邊我們說到了,若是咱們自定義中間件的話,在中間件中,咱們在 Claims 添加了角色的相關權限:
並且很天然的在 接口中,也是分爲兩種狀況:要麼沒有加權限,要麼就是基於角色的加權:
可是若是這個時候,咱們直接對接口增長 無任何策略 的加權:
就是沒有任何的策略,咱們登陸,而後添加 token,一看,仍是報錯了!具體的來看動圖:
原本 [Authorize] 這種 無策略 的受權,按理說只須要咱們登陸了就能夠了,不須要其餘任何限制就能夠訪問,可是如今依然報錯401 ,證實咱們的中間件並不能對這種方案起到效果,你可能會問,那帶有 Roles=「Admin」 的爲啥能夠呢?反而這種無策略的不行呢,我我的感受可能仍是中間件我們設計的解決方案就是基於角色受權的那種,(我也再研究研究,看看能不能完善下這個自定義中間件,使它能適應這個 無具體策略 的加權方案,可是可能寫到最後,就是無限接近官方的受權中間件了哈哈)。
這個時候咱們發現,自定義中間件仍是挺麻煩的,可是你經過本身使用自定義受權中間件,不只僅能夠了解到中間件的使用,還能夠了解 netcore 究竟是如何受權的機制,可是我仍是建議你們使用官方的認證方案,畢竟他們考慮的很全面的。
那麼若是咱們想要用官方的認證方案呢,要怎麼寫呢?請往下看:
上邊我們說完了自定義中間件的形式,發現了也方便的地方,也有不方便之處,雖然靈活的使用了自定義身份驗證,可是畢竟很受限,並且也沒法對過時時間進行判斷,之後的文章你會看到《36 ║解決JWT自定義中間件受權過時問題》,這裏先不說,重點說說,如何經過官方認證來實現。
和上邊自定義的過程如出一轍,略。
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
//2.1【認證】 services.AddAuthentication(x => { //看這個單詞熟悉麼?沒錯,就是上邊錯誤裏的那個。 x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; })// 也能夠直接寫字符串,AddAuthentication("Bearer") .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey,//參數配置在下邊 ValidateIssuer = true, ValidIssuer = audienceConfig["Issuer"],//發行人 ValidateAudience = true, ValidAudience = audienceConfig["Audience"],//訂閱人 ValidateLifetime = true, ClockSkew = TimeSpan.Zero, RequireExpirationTime = true, }; });
上邊代碼中出現的部分參數定義(若是還看不懂,請看項目代碼):
#region 參數 //讀取配置文件 var audienceConfig = Configuration.GetSection("Audience"); var symmetricKeyAsBase64 = audienceConfig["Secret"]; var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64); var signingKey = new SymmetricSecurityKey(keyByteArray); var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
具體的每一個配置的含義呢,個人代碼裏都有,你們本身能夠看看,都很簡單。
劃重點:咱們就是用的這個官方默認的方案,來替換了咱們自定義中間件的身份驗證方案,從而達到目的,說白了,就是官方封裝了一套方案,這樣咱們就不用寫中間件了。
//若是你想使用官方認證,必須在上邊ConfigureService 中,配置JWT的認證服務 (.AddAuthentication 和 .AddJwtBearer 兩者缺一不可) app.UseAuthentication();
這樣就完成了,結果也不用看了,你們自行測試便可,不管添加或者不添加 token ,都不會報錯。
若是對 claim[] 定義不是很理解,能夠看看dudu大神的解釋《理解ASP.NET Core驗證模型(Claim, ClaimsIdentity, ClaimsPrincipal)不得不讀的英文博文》:
這篇英文博文是 Andrew Lock 寫的 Introduction to Authentication with ASP.NET Core 。 如下是簡單的閱讀筆記: ----------------------------------- ASP.NET Core 的驗證模型是 claims-based authentication 。Claim 是對被驗證主體特徵的一種表述,好比:登陸用戶名是...,email是...,用戶Id是...,其中的「登陸用戶名」,「email」,「用戶Id」就是ClaimType。 You can think of claims as being a statement about...That statement consists of a name and a value. 對應現實中的事物,好比駕照,駕照中的「身份證號碼:xxx」是一個claim,「姓名:xxx」是另外一個claim。 一組claims構成了一個identity,具備這些claims的identity就是 ClaimsIdentity ,駕照就是一種ClaimsIdentity,能夠把ClaimsIdentity理解爲「證件」,駕照是一種證件,護照也是一種證件。 ClaimsIdentity的持有者就是 ClaimsPrincipal ,一個ClaimsPrincipal能夠持有多個ClaimsIdentity,就好比一我的既持有駕照,又持有護照。 ------------------------------------ 理解了Claim, ClaimsIdentity, ClaimsPrincipal這三個概念,就能理解生成登陸Cookie爲何要用下面的代碼? var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, loginName) }, "Basic"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); await context.Authentication.SignInAsync(_cookieAuthOptions.AuthenticationScheme, claimsPrincipal); 要用Cookie表明一個經過驗證的主體,必須包含Claim, ClaimsIdentity, ClaimsPrincipal這三個信息,以一個持有合法駕照的人作比方,ClaimsPrincipal就是持有證件的人,ClaimsIdentity就是證件,"Basic"就是證件類型(這裏假設是駕照),Claim就是駕照中的信息。
一、而後再Startup的Configure中,將TokenAuth註冊中間件
注意1:HTTP管道是有前後順序的,必定要寫在 app.Mvc() 以前,不然不起做用。
注意2:這裏咱們是自定義了認證中間件,來對JWT的字符串進行自定義受權認證,因此上邊都很正常,甚至咱們的Token能夠不用帶 Bearer 特定字符串,若是你之後遇到了使用官方認證中間件 UseAuthentication(),那麼就必須在 configureService 中對認證進行配置(並且Token傳遞的時候,也必須帶上"Bearer " 這樣的特定字符串,這也就是解釋了上文,爲啥要帶Bearer),這裏先打個預防針,由於個人最新 Github 上已經使用了官方的認證中間件,因此除了上邊配置的那些服務外,還須要配置 Service.AddAuthentication 和 Service.AddJwtBearer 兩個服務。
// 若是你想使用官方認證,必須在上邊ConfigureService 中,配置JWT的認證服務 // .AddAuthentication 和 .AddJwtBearer 兩者缺一不可 app.UseAuthentication();
若是你感受上邊沒看懂,繼續用下邊的知識點來鞏固吧!
如下是參考大神文章:@ASP.NET Core 認證與受權[4]:JwtBearer認證 ,必定要多看多想,下邊的代碼我沒有試驗正確性,你們看個意思便可,不用糾結正確與否,重點跟着這個系列日後走就行。
HTTP提供了一套標準的身份驗證框架:服務器能夠用來針對客戶端的請求發送質詢(challenge),客戶端根據質詢提供身份驗證憑證。質詢與應答的工做流程以下:服務器端向客戶端返回401(Unauthorized,未受權)狀態碼,並在WWW-Authenticate頭中添加如何進行驗證的信息,其中至少包含有一種質詢方式。而後客戶端能夠在請求中添加Authorization頭進行驗證,其Value爲身份驗證的憑證信息。
在HTTP標準驗證方案中,咱們比較熟悉的是"Basic"和"Digest",前者將用戶名密碼使用BASE64編碼後做爲驗證憑證,後者是Basic的升級版,更加安全,由於Basic是明文傳輸密碼信息,而Digest是加密後傳輸。在前文介紹的Cookie認證屬於Form認證,並不屬於HTTP標準驗證。
本文要介紹的Bearer驗證也屬於HTTP協議標準驗證,它隨着OAuth協議而開始流行,詳細定義見: 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
,它的頒發和驗證徹底由咱們本身的應用程序來控制,而不依賴於系統和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
。
上面介紹的Bearer認證,其核心即是BEARER_TOKEN,而最流行的Token編碼方式即是:JSON WEB TOKEN。
Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT是由.
分割的以下三部分組成:
Header 通常由兩個部分組成:
alg
是是所使用的hash算法,如:HMAC SHA256或RSA,typ
是Token的類型,在這裏就是:JWT。
{
"alg": "HS256", "typ": "JWT" }
而後使用Base64Url編碼成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
這一部分是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是用來驗證發送者的JWT的同時也能確保在期間不被篡改。
在建立該部分時候你應該已經有了編碼後的Header和Payload,而後使用保存在服務端的祕鑰對其簽名,一個完整的JWT以下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
所以使用JWT具備以下好處:
通用:由於json的通用性,因此JWT是能夠進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等不少語言均可以使用。
緊湊:JWT的構成很是簡單,字節佔用很小,能夠經過 GET、POST 等放在 HTTP 的 header 中,很是便於傳輸。
擴展:JWT是自我包涵的,包含了必要的全部信息,不須要在服務端保存會話信息, 很是易於應用的擴展。
關於更多JWT的介紹,網上很是多,這裏就再也不多作介紹。下面,演示一下 ASP.NET Core 中 JwtBearer 認證的使用方式。
ASP.NET Core 內置的JwtBearer驗證,並不包含Token的發放,咱們先模擬一個簡單的實現:
[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),能夠很容易的建立出JwtToen,就再也不多說。
首先添加JwtBearer
包引用:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 2.0.0
而後在Startup
類中添加以下配置:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, ValidIssuer = "http://localhost:5200", ValidAudience = "api", IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Consts.Secret)) /***********************************TokenValidationParameters的參數默認值***********************************/ // RequireSignedTokens = true, // SaveSigninToken = false, // ValidateActor = false, // 將下面兩個參數設置爲false,能夠不驗證Issuer和Audience,可是不建議這樣作。 // ValidateAudience = true, // ValidateIssuer = true, // ValidateIssuerSigningKey = false, // 是否要求Token的Claims中必須包含Expires // RequireExpirationTime = true, // 容許的服務器時間偏移量 // ClockSkew = TimeSpan.FromSeconds(300), // 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比 // ValidateLifetime = true }; }); } public void Configure(IApplicationBuilder app) { app.UseAuthentication(); }
在JwtBearerOptions
的配置中,一般IssuerSigningKey(簽名祕鑰)
, ValidIssuer(Token頒發機構)
, ValidAudience(頒發給誰)
三個參數是必須的,後二者用於與TokenClaims中的Issuer
和Audience
進行對比,不一致則驗證失敗(與上面發放Token中的Claims對應)。
而NameClaimType
和RoleClaimType
需與Token中的ClaimType一致,在IdentityServer中也是使用的JwtClaimTypes
,不然會形成User.Identity.Name
爲空等問題。
建立一個須要受權的控制器,直接使用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":"Chilly","temperatureF":94}]
JwtBearer認證中,默認是經過Http的Authorization
頭來獲取的,這也是最推薦的作法,可是在某些場景下,咱們可能會使用Url或者是Cookie來傳遞Token,那要怎麼來實現呢?
其實實現起來很是簡單,如前幾章介紹的同樣,JwtBearer也在認證的各個階段爲咱們提供了事件,來執行咱們的自定義邏輯:
.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: 未受權時調用。
在上面的示例中,咱們簡單模擬的Token頒發,功能很是簡單,並不適合在生產環境中使用,但是微軟也沒有提供OIDC服務的實現,好在.NET社區中提供了幾種實現,可供咱們選擇:
Name | Description |
---|---|
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服務器,並添加以下配置:
/********************OIDC服務器代碼片斷********************/ public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // 配置IdentitryServer services.AddIdentityServer() .AddInMemoryPersistedGrants() .AddInMemoryApiResources(Config.GetApis()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryClients(Config.GetClients()) .AddTestUsers(Config.GetUsers()) .AddDeveloperSigningCredential(); } new Client { ClientId = "jwt.implicit", ClientName = "Implicit Client (Web)", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, RedirectUris = { "http://localhost:5200/callback" }, PostLogoutRedirectUris = { "http://localhost:5200/home" }, AllowedCorsOrigins = { "http://localhost:5200" }, AllowedScopes = { "openid", "profile", "email", "api" }, }
而JwtBearer客戶端的配置就更加簡單了,由於OIDC具備配置發現的功能:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.Authority = "https://oidc.faasx.com/"; o.Audience = "api"; o.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role, }; }); }
如上,最重要的是Authority
參數,用來表示OIDC服務的地址,而後即可以自動發現Issuer
, IssuerSigningKey
等配置,而o.Audience
與o.TokenValidationParameters = new TokenValidationParameters { ValidAudience = "api" }
是等效的,後面分析源碼時會介紹。
OIDC兼容OAuth2協議,咱們可使用上一章介紹的受權碼模式來獲取Token,也能夠直接用戶名密碼模式來獲取Token:
請求: POST https://oidc.faasx.com/connect/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded client_id=client.rop&client_secret=secret&grant_type=password&scope=api&username=alice&password=alice 響應: HTTP/1.1 200 OK Content-Type: application/json {"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlYzk5MjVlMmUzMTA2NmY2ZmU2ODgzMDRhZjU1ZmM0IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDk2NzI1NjksImV4cCI6MTUwOTY3NjE2OSwiaXNzIjoiaHR0cHM6Ly9vaWRjLmZhYXN4LmNvbSIsImF1ZCI6WyJodHRwczovL29pZGMuZmFhc3guY29tL3Jlc291cmNlcyIsImFwaSJdLCJjbGllbnRfaWQiOiJjbGllbnQucm9wIiwic3ViIjoiMDAxIiwiYXV0aF90aW1lIjoxNTA5NjcyNTY5LCJpZHAiOiJsb2NhbCIsIm5hbWUiOiJBbGljZSBTbWl0aCIsImVtYWlsIjoiQWxpY2VTbWl0aEBlbWFpbC5jb20iLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbInB3ZCJdfQ.PM93LThOZA3lkgPFVwieqGQQQtgmYDCY0oSFVmudv1hpKO6UaaZsmnn4ci9QjbGl5g2433JkDks5UIZsZ0xE62Qqq8PicPBBuaNoYrCf6dxR7j-0uZcoa7-FCKGu-0TrM8OL-NuMvN6_KEpbWa3jlkwibCK9YDIwJZilVoWUOrbbIEsKTa-DdLScmzHLUzksT8GBr0PAVhge9PRFiGqg8cgMLjsA62ZeDsR35f55BucSV5Pj0SAj26anYvrBNTHKOF7ze1DGW51Dbz6DRu1X7uEIxSzWiNi4cRVJ6Totjkwk5F78R9R38o_mYEdehZBjRHFe6zLd91hXcCKqOEh5eQ","expires_in":3600,"token_type":"Bearer"}
咱們使用https://jwt.io解析一下OIDC服務器頒發的Token中的Claims:
{
"nbf": 1509672569, // 2017/11/3 1:29:29 NotBefore Token生效時間,在此以前不可用 "exp": 1509676169, // 2017/11/3 2:29:29 Expiration Token過時時間,在此以後不可用 "iss": "https://oidc.faasx.com", // Issuer 頒發者,一般爲STS服務器地址 "aud": [ // Audience Token的做用對象,也就是被訪問的資源服務器受權標識 "https://oidc.faasx.com/resources", "api" ], "client_id": "client.rop", // 客戶端標識 "sub": "001", "auth_time": 1509672569, // Token頒發時間 "idp": "local", "name": "Alice Smith", "email": "AliceSmith@email.com", "scope": [ "api" ], "amr": [ "pwd" ] }
這一篇呢,寫的比較潦草,主要是講如何使用,具體的細節知識,仍是你們摸索,仍是那句話,這裏只是拋磚引玉的做用喲,經過閱讀本文,你會了解到,什麼是JWT,如何添加配置.net core 中間件,如何使用Token驗證,在之後的項目裏你就能夠在登陸的時候,調用Token,返回客戶端,而後判斷是否有相應的接口權限。
一、JWT裏會存在一些用戶的信息,好比用戶id、角色role 等等,這樣會不會不安全,信息被泄露?
答:JWT 原本就是一種無狀態的登陸受權認證,用來替代每次請求都須要輸入用戶名+密碼的尷尬狀況,存在一些不重要的明文很正常,只要不把隱私放出去就行,就算是被動機不良的人獲得,也作不了什麼事情。
二、生成 JWT 的時候須要 secret ,可是 解密的時候 爲啥沒有用到 secret ?
答:secret的做用,主要是用來防止 token 被僞造和篡改的,想一想上邊的那個第一個問題,用戶獲得了你的令牌,獲取到了你的我的信息,這個是沒事兒的,他什麼也幹不了,可是若是用戶本身隨便的生成一個 token ,帶上你的uid,豈不是隨便就能夠訪問資源服務器了,因此這個時候就須要一個 secret 來生成 token,這樣的話,就能保證數字簽名的正確性。
並且,在咱們資源服務器裏,將token解析的時候,微軟封裝了方法,將secret進行校驗了,這就是保證了token的安全性,從而保證咱們的資源api是安全的,你不信的話,能夠用你網站的 token 來訪問個人在線項目,就算是 uid,role等等所有正確,仍是不能訪問個人網站,由於你不知道個人secret,因此你生成的令牌對個人是無效的。
好啦!項目準備階段就這麼結束了,之後我們就能夠直接用swagger來調試了,而不是每次都用F5運行等,接下來咱們就要正式開始搭建項目了,主要採用的是泛型倉儲模式 Repository+Service,也是一種常見的模式。
本系列開源地址
https://github.com/anjoy8/Blog.Core.git
本文章小Demo
https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_JWT