認證授權方案之JwtBearer認證

艾三元發表於2020-06-19

1.前言

回顧認證方案之初步認識JWT

在現代Web應用程式中,即分為前端與後端兩大部分。當前前後端的趨勢日益劇增,前端裝置(手機、平板、電腦、及其他裝置)層出不窮。因此,為了方便滿足前端裝置與後端進行通訊,就必須有一種統一的機制。所以導致API架構的流行。而RESTful API這個API設計思想理論也就成為目前網際網路應用程式比較歡迎的一套方式。

這種API架構思想的引入,因此,我們就需要考慮用一種標準的,通用的,無狀態的,與語言無關的身份認證方式來實現API介面的認證。

HTTP提供了一套標準的身份驗證框架:服務端可以用來針對客戶端的請求傳送質詢(challenge),客戶端根據質詢提供應答身份驗證憑證。

質詢與應答的工作流程如下:服務端向客戶端返回401(Unauthorized,未授權)狀態碼,並在WWW-Authenticate頭中新增如何進行驗證的資訊,其中至少包含有一種質詢方式。然後客戶端可以在請求中新增Authorization頭進行驗證,其Value為身份驗證的憑證資訊。

在本文中,將要介紹的是以Jwt Bearer方式進行認證。
JwtBearer

2.Bearer認證

本文要介紹的Bearer驗證也屬於HTTP協議標準驗證,它隨著OAuth協議而開始流行,詳細定義見: RFC 6570

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

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認證的核心是Token,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

3.JWT

上面介紹的Bearer認證,其核心便是BEARER_TOKEN,那麼,如何確保Token的安全是重中之重。一種是通過HTTPS的方式,另一種是通過對Token進行加密編碼簽名,而最流行的Token編碼簽名方式便是:JSON WEB TOKEN。

Json web token (Jwt), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。

JWT是由.分割的如下三部分組成:

Header.Payload.Signature

JwtBearer

還記得之前說個的一篇認證方案之初步認識JWT嗎?沒有的,可以看看,對JWT的特點和基本原理介紹,可以進一步的瞭解。

學習了之前的文章後,我們可以發現使用JWT的好處在於通用性、緊湊性和可擴充性。

  • 通用性:因為json的通用性,所以JWT是可以進行跨語言支援的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
  • 緊湊性:JWT的構成非常簡單,位元組佔用很小,通過 GET、POST 等放在 HTTP 的 header 中,便於傳輸。
  • 可擴充套件性:JWT是自我包涵的,因為有了payload部分,包含了必要的一些其他業務邏輯所必要的非敏感資訊,自身儲存,不需要在服務端儲存會話資訊, 非常易於應用的擴充套件。

4.開始

1. 註冊認證服務

在這裡,我們用微軟給我們提供的JwtBearer認證方式,實現認證服務註冊 。

引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

註冊服務,將服務新增到容器中,

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        var Issurer = "JWTBearer.Auth";  //發行人
        var Audience = "api.auth";       //受眾人
        var secretCredentials = "q2xiARx$4x3TKqBJ";   //金鑰

        //配置認證服務
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(o=>{
            o.TokenValidationParameters = new TokenValidationParameters
            {
                //是否驗證發行人
                ValidateIssuer = true,
                ValidIssuer = Issurer,//發行人
                //是否驗證受眾人
                ValidateAudience = true,
                ValidAudience = Audience,//受眾人
                //是否驗證金鑰
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)),
                
                ValidateLifetime = true, //驗證生命週期
                RequireExpirationTime = true, //過期時間
            };
        });
    }

注意說明:

一. TokenValidationParameters的引數預設值:
1. ValidateAudience = true,  ----- 如果設定為false,則不驗證Audience受眾人
2. ValidateIssuer = true ,   ----- 如果設定為false,則不驗證Issuer釋出人,但建議不建議這樣設定
3. ValidateIssuerSigningKey = false,
4. ValidateLifetime = true,  ----- 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比
5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必須包含Expires
6. ClockSkew = TimeSpan.FromSeconds(300), ----- 允許伺服器時間偏移量300秒,即我們配置的過期時間加上這個允許偏移的時間值,才是真正過期的時間(過期時間 +偏移值)你也可以設定為0,ClockSkew = TimeSpan.Zero

呼叫方法,配置Http請求管道:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        //1.先開啟認證
        app.UseAuthentication();
        //2.再開啟授權
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

JwtBearerOptions的配置中,通常IssuerSigningKey(簽名祕鑰), ValidIssuer(Token頒發機構), ValidAudience(頒發給誰) 三個引數是必須的,後兩者用於與TokenClaims中的IssuerAudience進行對比,不一致則驗證失敗。

2.介面資源保護

建立一個需要授權保護的資源控制器,這裡我們用建立API生成專案自帶的控制器,WeatherForecastController.cs, 在控制器上使用Authorize即可

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

3. 生成Token

因為微軟為我們內建了JwtBearer驗證,但是沒有提供Token的發放,所以這裡我們要實現生成Token的方法

引入Nugets包:System.IdentityModel.Tokens.Jwt

這裡我們根據IdentityModel.Tokens.Jwt文件給我們提供的幫助類,提供了方法WriteToken建立Token,根據引數SecurityToken,可以例項化,JwtSecurityToken,指定可選引數的類。

        /// <summary>
        /// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters.
        /// </summary>
        /// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param>
        /// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param>
        /// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List&lt;object&gt; } will be created to contain the duplicate values.</param>
        /// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param>
        /// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param>
        /// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param>
        /// <exception cref="ArgumentException">If 'expires' &lt;= 'notbefore'.</exception>
        public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null)
        {
            if (expires.HasValue && notBefore.HasValue)
            {
                if (notBefore >= expires)
                    throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value)));
            }

            Payload = new JwtPayload(issuer, audience, claims, notBefore, expires);
            Header = new JwtHeader(signingCredentials);
            RawSignature = string.Empty;
        }

這樣,我們可以根據引數指定內容:

1. string iss = "JWTBearer.Auth";  // 定義發行人
2. string aud = "api.auth";       //定義受眾人audience
3. IEnumerable<Claim> claims = new Claim[]
{
new Claim(JwtClaimTypes.Id,"1"),
new Claim(JwtClaimTypes.Name,"i3yuan"),
};//定義許多種的宣告Claim,資訊儲存部分,Claims的實體一般包含使用者和一些後設資料
4. var nbf = DateTime.UtcNow;  //notBefore  生效時間
5. var Exp = DateTime.UtcNow.AddSeconds(1000);  //expires 過期時間
6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字元
 var secret = Encoding.UTF8.GetBytes(sign);
 var key = new SymmetricSecurityKey(secret);
 var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

好了,通過以上填充引數內容,進行傳參賦值得到,完整程式碼如下:

新增AuthController.cs控制器:

    [HttpGet]
    public IActionResult GetToken()
    {
        try
        {
            //定義發行人issuer
            string iss = "JWTBearer.Auth";
            //定義受眾人audience
            string aud = "api.auth";

            //定義許多種的宣告Claim,資訊儲存部分,Claims的實體一般包含使用者和一些後設資料
            IEnumerable<Claim> claims = new Claim[]
            {
                new Claim(JwtClaimTypes.Id,"1"),
                new Claim(JwtClaimTypes.Name,"i3yuan"),
            };
            //notBefore  生效時間
            // long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
            var nbf = DateTime.UtcNow;
            //expires   //過期時間
            // long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds();
            var Exp = DateTime.UtcNow.AddSeconds(1000);
            //signingCredentials  簽名憑證
            string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的長度必須 大於等於 16個字元
            var secret = Encoding.UTF8.GetBytes(sign);
            var key = new SymmetricSecurityKey(secret);
            var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds);
		    var JwtHander = new JwtSecurityTokenHandler();
            var token = JwtHander.WriteToken(jwt);
            return Ok(new
            {
                access_token = token,
                token_type = "Bearer",
            });
        }
        catch (Exception ex)
        {
            throw;
        }
    }
注意:
1.SecurityKey 的長度必須 大於等於 16個字元,否則生成會報錯。(可通過線上隨機生成金鑰)

5. 執行

訪問獲取Token方法,獲取得到access_token:

JwtBearer

再訪問,授權資源介面,可以發現,再沒有新增請求頭token值的情況下,返回了401沒有許可權。

JwtBearer

這次,在請求頭通過Authorization加上之前獲取的token值後,再次進行訪問,發現已經可以獲取訪問資源控制器,並返回對應的資料。

JwtBearer

6.擴充套件說明

在HTTP標準驗證方案中,我們比較熟悉的是"Basic"和"Digest",前者將使用者名稱密碼使用BASE64編碼後作為驗證憑證,後者是Basic的升級版,更加安全,因為Basic是明文傳輸密碼資訊,而Digest是加密後傳輸。

一、Basic基礎認證

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸使用者名稱和密碼到服務端進行認證,通常需要配合HTTPS來保證資訊傳輸的安全。

客戶端請求需要帶Authorization請求頭,值為“Basic xxx”,xxx為“使用者名稱:密碼”進行Base64編碼後生成的值。 若客戶端是瀏覽器,則瀏覽器會提供一個輸入使用者名稱和密碼的對話方塊,使用者輸入使用者名稱和密碼後,瀏覽器會儲存使用者名稱和密碼,用於構造Authorization值。當關閉瀏覽器後,使用者名稱和密碼將不再儲存。

憑證為“YWxhzGRpbjpvcGVuc2VzYWl1”,是通過將“使用者名稱:密碼”格式的字串經過的Base64編碼得到的。而Base64不屬於加密範疇,可以被逆向解碼,等同於明文,因此Basic傳輸認證資訊是不安全的。

Basic基礎認證圖示:

JwtBearer

缺陷彙總
1.使用者名稱和密碼明文(Base64)傳輸,需要配合HTTPS來保證資訊傳輸的安全。
2.即使密碼被強加密,第三方仍可通過加密後的使用者名稱和密碼進行重放攻擊。
3.沒有提供任何針對代理和中間節點的防護措施。
4.假冒伺服器很容易騙過認證,誘導使用者輸入使用者名稱和密碼。

二、Digest摘要認證

Digest認證是為了修復基本認證協議的嚴重缺陷而設計的,秉承“絕不通過明文在網路傳送密碼”的原則,通過“密碼摘要”進行認證,大大提高了安全性。

Digest認證步驟如下:
第一步:客戶端訪問Http資源伺服器。由於需要Digest認證,伺服器返回了兩個重要欄位nonce(隨機數)和realm。
第二步:客戶端構造Authorization請求頭,值包含username、realm、nouce、uri和response的欄位資訊。其中,realm和nouce就是第一步返回的值。nouce只能被服務端使用一次。uri(digest-uri)即Request-URI的值,但考慮到經代理轉發後Request-URI的值可能被修改、因此實現會複製一份副本儲存在uri內。response也可叫做Request-digest,存放經過MD5運算後的密碼字串,形成響應碼。
第三步:伺服器驗證包含Authorization值的請求,若驗證通過則可訪問資源。
Digest認證可以防止密碼洩露和請求重放,但沒辦法防假冒。所以安全級別較低。
Digest和Basic認證一樣,每次都會傳送Authorization請求頭,也就相當於重新構造此值。所以兩者易用性都較差。

Digest認證圖示:
JwtBearer

7.注意

  1. 在進行JwtBearer認證時,在生成token之後,還需要與重新整理token配合使用,因為當使用者執行了退出,修改密碼等操作時,需要讓該token無效,無法再次使用,所以,會給access_token設定一個較短的有效期間,(JwtBearer認證預設會驗證有效期,通過notBeforeexpires來驗證),當access_token過期後,可以在使用者無感知的情況下,使用refresh_token重新獲取access_token,但這就不屬於Bearer認證的範疇了,但是我們可以通過另一種方式通過IdentityServer的方式來實現,在後續中會對IdentityServer進行詳細講解。
  2. 在生成token的時候,需要用的secret,主要是用來防止token被偽造與篡改。因為當token被劫取的時候,可以得到你的令牌中帶的一些個人不重要的資訊明文,但不用擔心,只要你不在生成token裡把私密的個人資訊放出去的話,就算被動機不良的人得到,也做不了什麼事情。但是你可能會想,如果使用者自己隨便的生成一個 token ,帶上你的資訊,那不就可以隨便訪問你的資源伺服器了,因此這個時候就需要利用secret 來生成 token,來確保數字簽名的正確性。而且在認證授權資源,進行token解析的時候,通過微軟的原始碼發現,已經幫我們封裝了方法,對secret進行了校驗了,確保了token的安全性,從而保證api資源的安全。

8.總結

  1. JwtToken在認證時,無需Security token service安全令牌伺服器的參與,都是基於Claim的,預設會驗證有效期,通過notBeforeexpires來驗證,這在分散式中提供給了極大便利。
  2. JwtToken與平臺、無言無關,在前端也可以直接解析出Claims。
  3. 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
  4. 後面會對認證授權方案中的授權這一塊進行說明分享。
  5. 本示例原始碼地址
    參考JwtBearer原始碼

相關文章