微服務系列之授權認證(三) JWT

2022-09-21 18:00:42

1.JWT簡介

  官方定義:JWT是JSON Web Token的縮寫,JSON Web Token是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,可以將各方之間的資訊作為JSON物件安全地傳輸。該資訊可以被驗證和信任,因為它是經過加密的。

  實際上,Oauth2.0中的access token一般就是jwt格式。

  token由三部分組成,通過"."分隔,分別是:

      ● 檔頭

      ● 有效載荷

      ● 簽名

  所以JWT表示為:aaaaa.bbbbb.ccccc組成。

  1)檔頭,Header通常由兩部分組成:使用的加密演演算法 "alg" 以及Token的種類 "typ"。如下:

  

{
  "alg": "HS256",
  "typ": "JWT"
}

  此JSON被Base64Url編碼以形成JWT的第一部分。

  2)有效荷載,Payload主要包含了宣告Claims,宣告實際就是key:value資料,主要包含以下三種宣告:

  Registered Claims: 註冊宣告,為IANA JSON Web Token 登入檔中預先定義好的宣告,這些宣告非強制性,但是建議使用,如

  • ●  iss(issuer):簽發人

    ●  exp(expiration time) :過期時間

    ●  sub(subject):主題

    ●  aud(audience):受眾

    ●  nbf(not befaore):生效時間

    ●  lat(issued at):簽發時間

    ●  jti(jwt id):編號

  Public Claims:公共宣告,名稱可以被任意定義。為了防止重複,任何新的Claim名稱都應該被定義在IANA JSON Web Token Registry中或者使用一個包含不易重複名稱空間的URI。

  Private Claims:私有宣告,是在團隊中約定使用的自定義Claims,既不屬於Registered也不屬於Public。

  此JSON進行Base64Url編碼形成JWT的第二部分

  3)Signature,簽名是將第一部分(header)、第二部分(payload)、金鑰(key)通過指定演演算法(HMAC、RSA)進行加密生成的。

  

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

生成的簽名就是JWT的第三部分。

將這三部分拼接在一起並使用"."分隔後形成的字串就是Token。如:

可以使用jwt.io的Debugger解碼,驗證或生成JWT。

 

注意:雖然簽名過後的Token可以防止篡改,但是Token的資訊是公開的,任何人都可以讀取,所以儘量不要在有效載荷或檔頭傳遞敏感資訊(如密碼)。

2.使用場景

1)目前來說,幾乎所有之前使用cookie,session的地方,都可以換成jwt。

2)標準的對C或者對B的微服務系統,這個和之前講的Oauth2.0協定最大的不同之處,jwt只是一個傳輸令牌,oauth2.0是一個授權協定,jwt可以理解為是oauth2.0的一部分。。。

3)資訊保安交換,由於簽名防篡改機制,可以驗證其發行人和收件人。

3.Jwt的優勢

1)無需儲存,無伺服器壓力,輕量級使用,簡單上手。

2)無視跨域,可多端使用,不像cookie、session依賴瀏覽器。

4.前端滑動登入狀態管理方案

jwt token的過期時間如果短了,很影響前端使用者操作體驗,所以一般情況都是中長期的,以前的session管理登入狀態,是滑動的,而現在jwt是無狀態的,那麼怎麼才能做到滑動管理呢,具體細節分析請看這篇文章.NET Core WebAPI 認證授權之JWT(四):JWT續期問題 - 不落閣 (leo96.com) ,雖然沒有解決,但是問題丟擲的很細緻,,我來說說我們現在正在使用的方案。

其實也很簡單,使用者請求後端服務資料時,作為有效動作,並且觸發2小時的計時器,沒到2小時的定時器清除並且重新啟動,如果2小時內使用者沒有任何動作,認為是可以退出登入。

5..net core使用jwt

nuget安裝System.IdentityModel.Tokens.Jwt

新建一個service寫一個生成token的方法

 public class TokenService : ITokenService
    {
        private IConfiguration configuration;

        public TokenService(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        public async Task<string> MakeJwtToken(long userId)
        {
            var claims = new[]
                {
                 // 角色需要在這裡填寫
         new Claim(ClaimTypes.Role, "Admin"),
         // 多個角色可以重複寫,生成的 JWT 會是一個陣列
                  new Claim(ClaimTypes.Role, "SuperAdmin"),
                  //其他宣告
                  new Claim("uid", userId.ToString())
                };
            //私鑰,驗證方也需要使用這個進行驗證。
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Auth:SecurityKey"]));
            //加密方式
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: "AESCR",//發行人
                audience: "AESCR",//接收人
                claims: claims,
                expires: DateTime.Now.AddMonths(30),//過期時間
                signingCredentials: creds);
            var res = new JwtSecurityTokenHandler().WriteToken(token);
            return await Task.FromResult<string>(res);
        }
    }

啟動類認證注入

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,//是否驗證發行人
                        ValidateAudience = true,//是否驗證收件人
                        ValidateLifetime = true,//是否驗證失效時間
                        ValidateIssuerSigningKey = true,//是否驗證SecurityKey
                        ValidAudience = "AESCR",//Audience
                        ValidIssuer = "AESCR",//Issuer,這兩項和後面簽發jwt的設定一致
                        ClockSkew = TimeSpan.Zero, // // 預設允許 300s  的時間偏移量,設定為0
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Auth:SecurityKey"]))//與建立者金鑰一致
                    };
                });
  app.UseAuthentication();//新增認證中介軟體

控制器程式碼

/// <summary>
        /// 編輯使用者資訊
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="command"></param>
        /// <returns></returns>
        [HttpPost("{userId}/edit/userInfo")]
        [ProducesResponseType(typeof(Users), (int)HttpStatusCode.OK)]
        [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
        [Authorize]//token認證標籤,如果需要角色認證,[Authorize(Roles ="admin")]
        public async Task<IActionResult> EditUserInfo([FromRoute] long userId, [FromBody] EditUserInfoCommand command)
        {
            if (!this.CheckUser(command.UserId))
                return BadRequest("您沒有許可權存取");

            var result = await userService.EditUserInfo(command);
            if (!result.IsSuccess)
                return BadRequest(result.FailureReason);
            return Ok(result.GetData());
        }

以上程式碼就完事了,簡單吧,在請求的時候,帶上token就可以了。

這裡有一個細節問題,由於我們這裡使用者ID,都是在jwt的Payload中,那麼是否還需要請求介面的時候在引數中傳輸呢?個人理解是這樣:

1.首先先看下token驗證過後,怎麼取claims的宣告

 public static class ControllerExtensions
    {
        public static long GetUserId(this ControllerBase controllerBase)
        {
            var claim = controllerBase.User.Claims.Where(p => p.Type == "uid").FirstOrDefault();
            if (claim == null)
                return 0;
            long res = 0;
            long.TryParse(claim.Value, out res);
            return res;
        }

        public static bool CheckUser(this ControllerBase controllerBase, long userId)
        {
            return controllerBase.GetUserId() == userId;
        }
    }

我可以從payload中獲取到建立token時帶進去的使用者id---uid,回到問題,我認為即使可以拿到使用者ID,也需要從介面引數中傳遞過來,因為安全認證是一個切面攔截,我們的服務如果去掉切面,要保障正常執行,我們只需要在加一個傳遞引數中的uid和宣告裡的uid是否一致,來判斷是否是當前使用者的操作。