前言
最近沉寂了一段,主要是上半年相當於休息和調整了一段時間,接下來我將開始陸續學習一些新的技術,比如Docker、Jenkins等,都會以生活實例從零開始講解起,到時一並和大家分享和交流。接下來幾節課的內容將會講解JWT,關於JWT的原理解析等等園子里大有文章,就不再敘述,這里我們講解使用和一些注意的地方。
為什么要使用JWT
在.NET Core之前對於Web應用程序跟蹤用戶登錄狀態最普通的方式則是使用Cookie,當用戶點擊登錄后將對其信息進行加密並響應寫入到用戶瀏覽器的Cookie里,當用戶進行請求時,服務端將對Cookie進行解密,然后創建用戶身份,整個過程都是那么順其自然,但是這是客戶端是基於瀏覽器的情況,如果是客戶端是移動app或者桌面應用程序呢?關於JWT原理可以參考系列文章https://www.cnblogs.com/RainingNight/p/jwtbearer-authentication-in-asp-net-core.html,當然這只是其中一種限制還有其他。如果我們使用Json Web Token簡稱為JWT而不是使用Cookie,此時Token將代表用戶,同時我們不再依賴瀏覽器的內置機制來處理Cookie,我們僅僅只需要請求一個Token就好。這個時候就涉及到Token認證,那么什么是Token認證呢?一言以蔽之:將令牌(我們有時稱為AccessToken或者是Bearer Token)附加到HTTP請求中並對其進行身份認證的過程。Token認證被廣泛應用於移動端或SPA。
Json Web Token基礎
JWT由三部分構成,Base64編碼的Header,Base64編碼的Payload,簽名,三部分通過點隔開。第一部分以Base64編碼的Header主要包括Token的類型和所使用的算法,例如:
{ "alg": "HS265", "typ": "JWT" }
第二部分以Base64編碼的Payload主要包含的是聲明(Claims),例如,如下:
{ "sub": "765032130654732", "name": "jeffcky" }
第三部分則是將Key通過對應的加密算法生成簽名,最終三部分以點隔開,比如如下形式:
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. 2 eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSmVmZmNreSIsImVtYWlsIjoiMjc1MjE1NDg0NEBxcS5jb20iLCJleHAiOjE1NjU2MTUzOTgsIm5iZiI6MTU2MzE5NjE5OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIn0. 3 OJjlGJOnCCbpok05gOIgu5bwY8QYKfE2pOArtaZJbyI
到這里此時我們應該知道:JWT包含的信息並沒有加密,比如為了獲取Payload,我們大可通過比如谷歌控制台中的APi(atob)對其進行解碼,如下:
那如我所說既然JWT包含的信息並沒有加密,只是進行了Base64編碼,豈不是非常不安全呢?當然不是這樣,還沒說完,第三部分就是簽名,雖然我們對Payload(姑且翻譯為有效負載),未進行加密,但是若有蓄意更換Payload,此時簽名將能充分保證Token無效,除非將簽名的Key不小心暴露在光天化日之下,否則必須是安全的。好了,到了這里,我們稍稍講解了下JWT構成,接下來我們進入如何在.NET Core中使用JWT。
.NET Core中使用JWT
在.NET Core中如何使用JWT,那么我們必須得知曉如何創建JWT,接下來我們首先創建一個端口號為5000的APi,創建JWT,然后我們需要安裝 System.IdentityModel.Tokens.Jwt 包,如下:
我們直接給出代碼來創建Token,然后一一對其進行詳細解釋,代碼如下:
var claims = new Claim[] { new Claim(ClaimTypes.Name, "Jeffcky"), new Claim(JwtRegisteredClaimNames.Email, "2752154844@qq.com"), new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")); var token = new JwtSecurityToken( issuer: "http://localhost:5000", audience: "http://localhost:5001", claims: claims, notBefore: DateTime.Now, expires: DateTime.Now.AddHours(1), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) ); var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
如上我們在聲明集合中初始化聲明時,我們使用了兩種方式,一個是使用 ClaimTypes ,一個是 JwtRegisteredClaimNames ,那么這二者有什么區別?以及我們到底應該使用哪種方式更好?或者說兩種方式都使用是否有問題呢?針對ClaimTypes則來自命名空間 System.Security.Claims ,而JwtRegisteredClaimNames則來自命名空間 System.IdentityModel.Tokens.Jwt ,二者在獲取聲明方式上是不同的,ClaimTypes是沿襲微軟提供獲取聲明的方式,比如我們要在控制器Action方法上獲取上述ClaimTypes.Name的值,此時我們需要F12查看Name的常量定義值是多少,如下:
接下來則是獲取聲明Name的值,如下:
var sub = User.FindFirst(d => d.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
那么如果我們想要獲取聲明JwtRegisterClaimNames.Sub的值,我們是不是應該如上同樣去獲取呢?我們來試試。
var sub = User.FindFirst(d => d.Type == JwtRegisteredClaimNames.Sub)?.Value;
此時我們發現為空沒有獲取到,這是為何呢?這是因為獲取聲明的方式默認是走微軟定義的一套映射方式,如果我們想要走JWT映射聲明,那么我們需要將默認映射方式給移除掉,在對應客戶端Startup構造函數中,添加如下代碼:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
如果用過並熟悉IdentityServer4的童鞋關於這點早已明了,因為在IdentityServer4中映射聲明比如用戶Id即(sub)是使用的JWT,也就是說使用的JwtRegisteredClaimNames,此時我們再來獲取Sub看看。
所以以上對於初始化聲明兩種方式的探討並沒有用哪個更好,因為對於使用ClaimTypes是沿襲以往聲明映射的方式,如果要出於兼容性考慮,可以結合兩種聲明映射方式來使用。接下來我們來看生成簽名代碼,生成簽名是如下代碼:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
如上我們給出簽名的Key是1234567890123456,是不是給定Key的任意長度皆可呢,顯然不是,關於Key的長度至少是16,否則會拋出如下錯誤
接下來我們再來看實例化Token的參數,即如下代碼:
var token = new JwtSecurityToken( issuer: "http://localhost:5000", audience: "http://localhost:5001", claims: claims, notBefore: DateTime.Now, expires: DateTime.Now.AddHours(1), signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256) );
issuer代表頒發Token的Web應用程序,audience是Token的受理者,如果是依賴第三方來創建Token,這兩個參數肯定必須要指定,因為第三方本就不受信任,如此設置這兩個參數后,我們可驗證這兩個參數。要是我們完全不關心這兩個參數,可直接使用JwtSecurityToken的構造函數來創建Token,如下:
var claims = new Claim[] { new Claim(ClaimTypes.Name, "Jeffcky"), new Claim(JwtRegisteredClaimNames.Email, "2752154844@qq.com"), new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"), new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMilliseconds(1)).ToUnixTimeSeconds()}"), new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")); var jwtToken = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
這里需要注意的是Exp和Nbf是基於Unix時間的字符串,所以上述通過實例化DateTimeOffset來創建基於Unix的時間。到了這里,我們已經清楚的知道如何創建Token,接下來我們來使用Token獲取數據。我們新建一個端口號為5001的Web應用程序,同時安裝包【 Microsoft.AspNetCore.Authentication.JwtBearer 】接下來在Startup中ConfigureServices添加如下代碼:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")), ValidateIssuer = true, ValidIssuer = "http://localhost:5000", ValidateAudience = true, ValidAudience = "http://localhost:5001", ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(5) }; });
如上述若Token依賴於第三方而創建,此時必然會配置issuer和audience,同時在我方也如上必須驗證issuer和audience,上述我們也驗證了簽名,我們通過設置 ValidateLifetime 為true,說明驗證過期時間而並非Token中的值,最后設置 ClockSkew 有效期為5分鍾。對於設置 ClockSkew 除了如上方式外,還可如下設置默認也是5分鍾。
ClockSkew = TimeSpan.Zero
如上對於認證方案我們使用的是 JwtBearerDefaults.AuthenticationScheme 即Bearer,除此之外我們也可以自定義認證方案名稱,如下:
最后別忘記添加認證中間件在Configure方法中,認證中間件必須放在使用MVC中間件之前,如下:
app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
到了這里,我們通過端口為5000的Web Api創建了Token,並配置了端口號為5001的Web應用程序使用JWT認證,接下來最后一步則是調用端口號為5000的APi獲取Token,並將Token設置到請求頭中Authorization鍵的值,格式如下(注意Bearer后面有一個空格):
('Authorization', 'Bearer ' + token);
我們在頁面上放置一個按鈕點擊獲取端口號為5000的Token后,接下來請求端口號為5001的應用程序,如下:
$(function () { $('#btn').click(function () { $.get("http://localhost:5000/api/token").done(function (token) { $.ajax({ type: 'get', contentType: 'application/json', url: 'http://localhost:5001/api/home', beforeSend: function (xhr) { if (token !== null) { xhr.setRequestHeader('Authorization', 'Bearer ' + token); } }, success: function (data) { alert(data); }, error: function (xhr) { alert(xhr.status); } }); }); }); });
總結
本節我們講解了在.NET Core中使用JWT進行認證以及一點點注意事項,比較基礎性的東西,下一節講解完在JWT中使用刷新Token,開始正式進入Docker系列,感謝閱讀,下節見。