<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      為什么 退出登錄 或 修改密碼 無法使 token 失效

      前文說過 token 由 3 個部分組成,分別是
      metadata:加密方式;
      payload:token 業務層級的內容,例如 用戶id,token 過期時間,token 簽發人,token 簽發時間等內容;
      signature:對 payload 加密后的密文。
       
      一般地,當服務端接收到一個 token,是這樣校驗的:
      首先取出 signature 部分解密得到明文,再將明文比較于 token 中的 payload,如果二者一致,且當前時間處于 token 有效期內,則認為這是一個有效的 token。
       
      這樣做有個好處:服務端不需要另外去存儲這個 session 的狀態,只要校驗過 signature 有效,就可以拿到過期時間,簡化后端邏輯,做到 “無狀態化”。
      但這樣做也會有一個缺點,只要 token 簽發了,就無法更改這個 token 的內容
       

      問題場景

      一個用戶登錄,獲得一個有效的 token 之后,他點擊退出登錄。
      此時理想狀態下,我們希望這個 token 不再生效,但實際是,只要拿著這個 token 去訪問服務端 Authenticated 的資源,token 仍然會校驗通過。
      因為 [退出登錄] 的操作本身不能改變 token
       
       

      解決方案

      一、把 token 的有效期縮短,例如半小時或者五分鐘,但有個明顯的缺陷:用戶需要頻繁重新登錄。

       

      二、把 退出登錄 的用戶添加到 token black list 當中。

      簡單地,在調 sign out (退出登錄) 的 api 時,把用戶的 access token 添加到 token black list 當中;后端校驗 jwt 時,添加校驗 token 是否存在于 token black list 當中。
      下面展開設計過程:
       

      1. token black list 的設計

      是否持久化?

      第一個問題是:這種 sign out 的 token 要不要持久化?
      首先,token 本身就是會過期的;
      其次,這個新的校驗方法會作用到每一個通過了 token 有效驗證的請求,這個方法一定是高頻訪問的;
      所以,此處選擇通過 redis 緩存 tokenBlackList 。
      (Redis 是內存數據庫,支持高并發讀寫和自動過期(TTL),適合存儲臨時性黑名單數據。即使服務重啟,黑名單數據可能丟失,但 Token 本身有過期時間,因此不影響最終一致性。)
      每次 sign out,都將 set 到 redis 中;每次校驗 token,都查詢這個 redis value。
       

      數據結構設計

      REDIS 是 key,value 的鍵值對方式,value 可能是 string,list,hash..
      對于 value,可以直接粗暴的存儲整個 token json;
      那么 key 應該如何設計?使用 userId,那大概率會和其他業務的 redis key 重疊,在這里最好加上業務場景,形如,“TOKEN_BLACK_LIST_userId”。
       
      a. 多設備登錄場景
      假設:用戶 A 在設備 D1 上登錄后,在設備 D2 上同時登陸(這種場景當前是允許的);
      此時用戶 A 在設備 D1 上點擊 退出登錄,服務端會把 TOKEN_BLACK_LIST_AId : tokenJson 寫進了redis。
      此時用戶 A 再于設備 D2 上操作,校驗 token 時會去 redis 撈取數據,找到了 TOKEN_BLACK_LIST_AId,此時認為用戶 A 的 token 無效。
       
      如果這不是我們期望的場景,那應該如何讓同個賬號的多個 token 互不影響呢?此處 userId 就不適合作為 redis key
      是否每個 token 有自己特有的唯一的 id 呢?這又到 token payload 的組成,它確實存在唯一標識的 id,jti
      {
        "jti": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
        "iss": "the issuer",
        "aud": "the audience",
        "exp": 1630003600,
        "iat": 1630000000,
       ..
      }
      此故,這里把 key 設計成 [TOKEN_BLACK_LIST_tokenId],形如 "TOKEN_BLACK_LIST_a1b2c3d4-5678-90ef-ghij-klmnopqrstuv"。
       
       
      b. 修改密碼場景
      假設:用戶 A 在設備 D1、D2、D3 .. 多設備上均操作登錄,此時每個設備都持有一個獨立的 token;
      如果此時用戶在設備 D1 上 “修改密碼”,如何讓 D2、D3 等所有設備的登錄失效?
       
      后端可以把提出 “修改密碼” 的設備 D1 的 token 加入到 token_black_list 當中,但是如何知道這個用戶當前持有多少 token 呢?
      是否需要每次登錄都把 token 存儲起來?但這樣顯然會增加復雜度。
       
      我們可以重新審視一下 token 的結構,是否能找到一些屬性來使用?
      {
        "jti": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
        "iss": "the issuer",
        "aud": "the audience",
        "exp": 1630003600,
        "iat": 1630000000,
       ..
      }
      這里有一個非常巧妙而簡單的方式:
      每一個 token 上都會持有 iat 簽發時間,假設 用戶 A 在設備 D1 上確定 “修改密碼” 的時間是 changedPasswordDate
      服務端處理完 “修改密碼” 之后,可以把 changedPasswordDate 這個時間存儲到 redis 上,設其 key 為 TOKEN_INVALIDATION_userId,value 為 changedPasswordDate
       
      那么在服務端校驗 token 需要多添加這兩項校驗:
          查詢當前 token 是否存在于 TOKEN_BLACK_LIST 中
          查詢是否存在 TOKEN_INVALIDATION_userId,如果存在,
              比較當前 token 的 iat 時間是否早于 changedPasswordDate,如果是,該 token 無效
       
       

      2. code implement

      sign-out / change-password

      redis key-value 的過期時間取 token 的有效周期。本文設定 token 有效期為24小時,也即 1440 分鐘。
          public async Task<GlobalSignOutResponse> SignOutAsync(string accessToken)
          {
              var response = await _authService.SignOutAsync(accessToken);
              await _redisCacheService.SetCache(TokenHelper.GetRedisKeyForBlackAccessToken(accessToken), accessToken, 1440); // 分鐘單位
              return response;
          }
      "修改密碼" 的處理同理。
       

      jwt authentication

      startUp.cs

              services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                  .AddJwtBearer(options =>
                  {
                      ..
                      options.Events = new JwtBearerEvents
                      {
                          ..
                          OnTokenValidated = async context =>
                          {
                              if (await IsAccessTokenExpired(context, services))
                              {
                                  Log.Information($"The access token is expired as user already signed out or changed password.");
                                  context.Fail(GetTokenExpiredResponse(context.Response));
                              }
                              await Task.CompletedTask;
                          }
                      };
                  });

       

          private string GetTokenExpiredResponse(HttpResponse response)
          {
              if (ApiResponseCodes.AccessTokenExpired.BuildHttpResponse() is ObjectResult result)
              {
                  var payload = JObject.FromObject(result.Value);
                  response.ContentType = "application/json";
                  response.StatusCode = 401;
      
                  return payload.ToString();
              }
              return string.Empty;
          }
      
          private async Task<bool> IsAccessTokenExpired(TokenValidatedContext context, IServiceCollection services)
          {
              try
              {
                  var requestHeader = context.Request.Headers["Authorization"];
                  var accessToken = requestHeader.Count > 0 ? requestHeader[0].Split(" ")[1] : String.Empty;
                  var redisService = context.HttpContext.RequestServices.GetRequiredService<IRedisCacheService>()
                  var blackToken = await redisService.GetCache(TokenHelper.GetRedisKeyForBlackAccessToken(accessToken));
                  return blackToken == accessToken;
              }
              catch (Exception ex)
              {
                  Log.Error(ex, $"Failed to validate access token: {ex.Message}");
                  return true;
              }
          }
       
       
      * 由 [退出登錄] 無效化 token,還可以衍生出非常多的問題,此處暫且不表。
       
       
      ..
       
      posted @ 2025-03-05 09:19  Carcar019  閱讀(1204)  評論(9)    收藏  舉報
      主站蜘蛛池模板: 欧美xxxx精品另类| 国产一区二区三区禁18| 国产精品 无码专区| 亚洲中文字幕在线无码一区二区| 日韩中文字幕精品人妻| 92成人午夜福利一区二区| 亚洲av一本二本三本| 久久精品国产一区二区三| 人妻在线无码一区二区三区| 南涧| 色诱视频在线观看| 熟女一区二区中文字幕| 息烽县| 18禁黄无遮挡网站免费| 国产免费福利网站| 国产不卡一区不卡二区| 农村肥熟女一区二区三区| 麻豆国产传媒精品视频| 国产福利一区二区三区在线观看| 天堂a无码a无线孕交| 狠狠色噜噜狠狠狠狠777米奇| 又湿又紧又大又爽a视频| 国产日韩一区二区四季| 黄色亚洲一区二区在线观看| 妓院一钑片免看黄大片| 精品一区二区三区四区色| 国产一卡2卡三卡4卡免费网站| 兴和县| 亚洲第一无码专区天堂| 少妇人妻偷人精品视蜜桃 | 一区二区三区鲁丝不卡| 无码av中文字幕久久专区| 欧美日本在线一区二区三区| 日韩区中文字幕在线观看| 欧美丰满熟妇xxxx性| 天堂网国产| 性欧美乱熟妇xxxx白浆| 四虎国产精品永久入口| 日本夜爽爽一区二区三区| 日韩成人精品一区二区三区| 色偷偷女人的天堂亚洲网|