ASP.NET Core Web Api之JWT(一)

Jeffcky發表於2019-07-16

前言

最近沉寂了一段,主要是上半年相當於休息和調整了一段時間,接下來我將開始陸續學習一些新的技術,比如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系列,感謝閱讀,下節見。

相關文章