天天看點

js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

這篇文章主要介紹了詳解ASP.NET Core Web Api之JWT重新整理Token,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面随着小編來一起學習學習吧

前言

如題,本節我們進入JWT最後一節内容,JWT本質上就是從身份認證伺服器擷取通路令牌,繼而對于使用者後續可通路受保護資源,但是關鍵問題是:通路令牌的生命周期到底設定成多久呢?見過一些使用JWT的童鞋會将JWT過期時間設定成很長,有的幾個小時,有的一天,有的甚至一個月,這麼做當然存在問題,如果被惡意獲得通路令牌,那麼可在整個生命周期中使用通路令牌,也就是說存在冒充使用者身份,此時身份認證伺服器當然也就是始終信任該冒牌通路令牌,若要使得冒牌通路令牌無效,唯一的方案則是修改密鑰,但是如果我們這麼做了,則将使得已授予的通路令牌都将無效,是以更改密鑰不是最佳方案,我們應該從源頭盡量控制這個問題,而不是等到問題呈現再來想解決之道,重新整理令牌閃亮登場。

RefreshToken

什麼是重新整理令牌呢?重新整理通路令牌是用來從身份認證伺服器交換獲得新的通路令牌,有了重新整理令牌可以在通路令牌過期後通過重新整理令牌重新擷取新的通路令牌而無需用戶端通過憑據重新登入,如此一來,既保證了使用者通路令牌過期後的良好體驗,也保證了更高的系統安全性,同時,若通過重新整理令牌擷取新的通路令牌驗證其無效可将受訪者納入黑名單限制其通路,那麼通路令牌和重新整理令牌的生命周期設定成多久合适呢?這取決于系統要求的安全性,一般來講通路令牌的生命周期不會太長,比如5分鐘,又比如擷取微信的AccessToken的過期時間為2個小時。接下來我将用兩張表來示範實作重新整理令牌的整個過程,可能有更好的方案,歡迎在評論中提出,學習,學習。我們建立一個http://localhost:5000的WebApi用于身份認證,再建立一個http://localhost:5001的用戶端,首先點選【模拟登入擷取Toen】擷取通路令牌和重新整理令牌,然後點選【調用用戶端擷取目前時間】,如下:

js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

接下來我們建立一張使用者表(User)和使用者重新整理令牌表(UserRefreshToken),結構如下:

public class User
  {
    public string Id { get; set; }
    public string Email { get; set; }
    public string UserName { get; set; }

    private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();

    public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;

    /// <summary>
    /// 驗證重新整理token是否存在或過期
    /// </summary>
    /// <param name="refreshToken"></param>
    /// <returns></returns>
    public bool IsValidRefreshToken(string refreshToken)
    {
      return _userRefreshTokens.Any(d => (refreshToken) && );
    }

    /// <summary>
    /// 建立重新整理Token
    /// </summary>
    /// <param name="token"></param>
    /// <param name="userId"></param>
    /// <param name="minutes"></param>
    public void CreateRefreshToken(string token, string userId, double minutes = 1)
    {
      (new UserRefreshToken() { Token = token, UserId = userId, Expires = (minutes) });
    }

    /// <summary>
    /// 移除重新整理token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      ((t => t.Token == refreshToken));
    }
           
public class UserRefreshToken
  {
    public string Id { get; private set; } = ().ToString();
    public string Token { get; set; }
    public DateTime Expires { get; set; }
    public string UserId { get; set; }
    public bool Active => DateTime.Now <= Expires;
  }
           

如上可以看到對于重新整理令牌的操作我們将其放在使用者實體中,也就是使用EF Core中的Back Fields而不對外暴露。接下來我們将生成的通路令牌、重新整理令牌、驗證通路令牌、擷取使用者身份封裝成對應方法如下:

/// <summary>
    /// 生成通路令牌
    /// </summary>
    /// <param name="claims"></param>
    /// <returns></returns>
    public string GenerateAccessToken(Claim[] claims)
    {
      var key = new SymmetricSecurityKey((signingKey));

      var token = new JwtSecurityToken(
        issuer: "http://localhost:5000",
        audience: "http://localhost:5001",
        claims: claims,
        notBefore: DateTime.Now,
        expires: (1),
        signingCredentials: new SigningCredentials(key, )
      );

      return new JwtSecurityTokenHandler().WriteToken(token);
    }

    /// <summary>
    /// 生成重新整理Token
    /// </summary>
    /// <returns></returns>
    public string GenerateRefreshToken()
    {
      var randomNumber = new byte[32];
      using (var rng = ())
      {
        (randomNumber);
        return Convert.ToBase64String(randomNumber);
      }
    }

    /// <summary>
    /// 從Token中擷取使用者身份
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    public ClaimsPrincipal GetPrincipalFromAccessToken(string token)
    {
      var handler = new JwtSecurityTokenHandler();

      try
      {
        return handler.ValidateToken(token, new TokenValidationParameters
        {
          ValidateAudience = false,
          ValidateIssuer = false,
          ValidateIssuerSigningKey = true,
          IssuerSigningKey = new SymmetricSecurityKey((signingKey)),
          ValidateLifetime = false
        }, out SecurityToken validatedToken);
      }
      catch (Exception)
      {
        return null;
      }
    }
           

當使用者點選登入,通路身份認證伺服器,登入成功後我們建立通路令牌和重新整理令牌并傳回,如下:

[HttpPost("login")]
    public async Task<IActionResult> Login()
    {
      var user = new User()
      {
        Id = "D21D099B-B49B-4604-A247-71B0518A0B1C",
        UserName = "Jeffcky",
        Email = "[email protected]"
      };

      await context.Users.AddAsync(user);

      var refreshToken = GenerateRefreshToken();

      (refreshToken, );

      await context.SaveChangesAsync();

      var claims = new Claim[]
      {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, ),
      };

      return Ok(new Response() { AccessToken = GenerateAccessToken(claims), RefreshToken = refreshToken });
    }
           

此時我們回到如上給出的圖,我們點選【模拟登入擷取Token】,此時發出Ajax請求,然後将傳回的通路令牌和重新整理令牌存儲到本地localStorage中,如下:

<input type="button" value="模拟登入擷取Token" />

<input type="button" value="調用用戶端擷取目前時間" />
           
//模拟登陸
    $('#btn').click(function () {
      GetTokenAndRefreshToken();
    });

    //擷取Token
    function GetTokenAndRefreshToken() {  
     $.post('http://localhost:5000/api/account/login').done(function (data) {
        saveAccessToken();
        saveRefreshToken();
      });
    }
           
//從localStorage擷取AccessToken
    function getAccessToken() {
      return localStorage.getItem('accessToken');
    }

    //從localStorage擷取RefreshToken
    function getRefreshToken() {
      return localStorage.getItem('refreshToken');
    }

    //儲存AccessToken到localStorage
    function saveAccessToken(token) {
      ('accessToken', token);
    }

    //儲存RefreshToken到localStorage
    function saveRefreshToken(refreshToken) {
      ('refreshToken', refreshToken);
    }
           

此時我們再來點選【調用用戶端擷取目前時間】,同時将登入傳回的通路令牌設定到請求頭中,代碼如下:

$('#btn-currentTime').click(function () {
      GetCurrentTime();
    });

    //調用用戶端擷取目前時間
    function GetCurrentTime() {
      $.ajax({
        type: 'get',
        contentType: 'application/json',
        url: 'http://localhost:5001/api/home',
        beforeSend: function (xhr) {
          ('Authorization', 'Bearer ' + getAccessToken());
        },
        success: function (data) {
          alert(data);
        },
        error: function (xhr) {
          
        }
      });
    }
           

用戶端請求接口很簡單,為了讓大家一步步看明白,我也給出來,如下:

[Authorize]
    [HttpGet("api/[controller]")]
    public string GetCurrentTime()
    {
      return DateTime.Now.ToString("yyyy-MM-dd");
    }
           

好了到了這裡我們已經實作模拟登入擷取通路令牌,并能夠調用用戶端接口擷取到目前時間,同時我們也隻是傳回了重新整理令牌并存儲到了本地localStorage中,并未用到。當通路令牌過期後我們需要通過通路令牌和重新整理令牌去擷取新的通路令牌,對吧。那麼問題來了。我們怎麼知道通路令牌已經過期了呢?這是其一,其二是為何要發送舊的通路令牌去擷取新的通路令牌呢?直接通過重新整理令牌去換取不行嗎?有問題是好的,就怕沒有任何思考,我們一一來解答。我們在用戶端添加JWT中間件時,裡面有一個事件可以捕捉到通路令牌已過期(關于用戶端配置JWT中間件第一節已講過,這裡不再啰嗦),如下:

options.Events = new JwtBearerEvents
         {
           OnAuthenticationFailed = context =>
           {
             if (() == typeof(SecurityTokenExpiredException))
             {
               ("act", "expired");
             }
             return Task.CompletedTask;
           }
         };
           

通過如上事件并捕捉通路令牌過期異常,這裡我們在響應頭添加了一個自定義鍵act,值為expired,因為一個401隻能反映未授權,并不能代表通路令牌已過期。當我們在第一張圖中點選【調用用戶端擷取目前時間】發出Ajax請求時,如果通路令牌過期,此時在Ajax請求中的error方法中捕捉到,我們在如上已給出發出Ajax請求的error方法中繼續進行如下補充:

error: function (xhr) {
          if (xhr.status === 401 && xhr.getResponseHeader('act') === 'expired') {
            // 通路令牌肯定已過期
          }
        }
           

到了這裡我們已經解決如何捕捉到通路令牌已過期的問題,接下來我們需要做的則是擷取重新整理令牌,直接通過重新整理令牌換取新的通路令牌也并非不可,隻不過還是為了安全性考慮,我們加上舊的通路令牌。接下來我們發出Ajax請求擷取重新整理令牌,如下:

//擷取重新整理Token
    function GetRefreshToken(func) {
      var model = {
        accessToken: getAccessToken(),
        refreshToken: getRefreshToken()
      };
      $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: 'http://localhost:5000/api/account/refresh-token',
        dataType: "json",
        data: (model),
        success: function (data) {
          if (! && !) {
            // 跳轉至登入
          } else {
            saveAccessToken();
            saveRefreshToken();
            func();
          }
        }
      });
    }
           

發出Ajax請求擷取重新整理令牌的方法我們傳入了一個函數,這個函數則是上一次調用接口通路令牌過期的請求,點選【調用用戶端擷取目前時間】按鈕的Ajax請求error方法中,最終演變成如下這般:

error: function (xhr) {
          if (xhr.status === 401 && xhr.getResponseHeader('act') === 'expired') {

            /* 通路令牌肯定已過期,将目前請求傳入擷取重新整理令牌方法,
             * 以便擷取重新整理令牌換取新的令牌後繼續目前請求
            */
            GetRefreshToken(GetCurrentTime);
          }
        }
           

接下來則是通過傳入舊的通路令牌和重新整理令牌調用接口換取新的通路令牌,如下:

/// <summary>
    /// 重新整理Token
    /// </summary>
    /// <returns></returns>
    [HttpPost("refresh-token")]
    public async Task<IActionResult> RefreshToken([FromBody] Request request)
    {
      //TODO 參數校驗

      var principal = GetPrincipalFromAccessToken();

      if (principal is null)
      {
        return Ok(false);
      }

      var id = (c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;

      if ((id))
      {
        return Ok(false);
      }

      var user = await context.Users.Include(d => )
        .FirstOrDefaultAsync(d => d.Id == id);

      if (user is null || user.UserRefreshTokens?.Count() <= 0)
      {
        return Ok(false);
      }

      if (!())
      {
        return Ok(false);
      }

      ();

      var refreshToken = GenerateRefreshToken();

      (refreshToken, id);

      try
      {
        await context.SaveChangesAsync();
      }
      catch (Exception ex)
      {
        throw ex;
      }

      var claims = new Claim[]
      {
        new Claim(ClaimTypes.Name, user.UserName),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim(JwtRegisteredClaimNames.Sub, ),
      };

      return Ok(new Response()
      {
        AccessToken = GenerateAccessToken(claims),
        RefreshToken = refreshToken
      });
    }
           

如上通過傳入舊的通路令牌驗證并擷取使用者身份,然後驗證重新整理令牌是否已經過期,如果未過期則建立新的通路令牌,同時更新重新整理令牌。最終用戶端通路令牌過期的那一刻,通過重新整理令牌擷取新的通路令牌繼續調用上一請求,如下:

js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

到這裡關于JWT實作重新整理Token就已結束,自我感覺此種實作重新整理令牌将其存儲到資料庫的方案還算可取,将重新整理令牌存儲到Redis也可行,看個人選擇吧。上述若重新整理令牌驗證無效,可将通路者添加至黑名單,不過是添加一個屬性罷了。别着急,本節内容結束前,還留有彩蛋。

EntityFramework Core Back Fields深入探讨

無論是看視訊還是看技術部落格也好,一定要動手驗證,看到這裡覺得上述我所示範是不是毫無問題,如果閱讀本文的你直接拷貝上述代碼你會發現有問題,且聽我娓娓道來,讓我們來複習下Back Fields。Back Fields命名是有約定dei,上述我是根據約定而命名,是以千萬别一意孤行,别亂來,比如如下命名将抛出如下異常:

private readonly List<UserRefreshToken> _refreshTokens = new List<UserRefreshToken>();

 public IEnumerable<UserRefreshToken> UserRefreshTokens => _refreshTokens;
           
js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

上述我們配置重新整理令牌的Back Fields,代碼如下:

private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;
           
js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

要是我們配置成如下形式,結果又會怎樣呢?

private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => ();
           
js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

此時為了解決這個問題,我們必須将其顯式配置成Back Fields,如下:

protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      (u =>
      {
        var navigation = (nameof());
        ();
      });
    }
           

在我個人著作中也講解到為了性能問題,可将字段進行ToList(),若進行了ToList(),必須顯式配置成Back Fields,否則擷取不到重新整理令牌導航屬性,如下:

private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
public IEnumerable<UserRefreshToken> UserRefreshTokens => ();
           

或者進行如下配置,我想應該也可取,不會存在性能問題,如下:

private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IReadOnlyCollection<UserRefreshToken> UserRefreshTokens => ();
           

這是關于Back Fields問題之一,問題之二則是上述我們請求擷取重新整理令牌中,我們先在重新整理令牌的Back Fields中移除掉舊的重新整理令牌,而後再建立新的重新整理令牌,但是會抛出如下異常:

js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧
js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

我們看到在添加重新整理令牌時,使用者Id是有值的,對不對,這是為何呢?究其根本問題出在我們移除重新整理令牌方法中,如下:

/// <summary>
    /// 移除重新整理token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      ((t => t.Token == refreshToken));
    }
           

我們将查詢出來的導航屬性并将其映射到_userRefreshTokens字段中,此時是被上下文所追蹤,上述我們查詢出存在的重新整理令牌并在跟蹤的重新整理令牌中進行移除,沒毛病,沒找到原因,于是乎,我将上述方法修改成如下看看是否必須需要主鍵才能删除舊的重新整理令牌:

/// <summary>
    /// 移除重新整理token
    /// </summary>
    /// <param name="refreshToken"></param>
    public void RemoveRefreshToken(string refreshToken)
    {
      var id = (t => t.Token == refreshToken).Id;
      (new UserRefreshToken() { Id = id });
    }
           

倒沒抛出異常,建立了一個新的重新整理令牌,但是舊的重新整理令牌卻沒删除,如下:

js-cookie設定token過期時間_詳解ASP.NET Core Web Api之JWT重新整理Token_實用技巧

至此未找到問題出在哪裡,目前版本為,難道不能通過Back Fields移除對象?這個問題待解決。

總結

本節我們重點講解了如何實作JWT重新整理令牌,并也略帶讨論了EF Core中Back Fields以及尚未解決的問題,至此關于JWT已結束,下節開始正式進入Docker小白系列,感謝閱讀。

到此這篇關于詳解ASP.NET Core Web Api之JWT重新整理Token的文章就介紹到這了,更多相關ASP.NET Core Web Api JWT重新整理Token内容請搜尋腳本之家以前的文章或繼續浏覽下面的相關文章希望大家以後多多支援腳本之家!

繼續閱讀