2017-11-10 418 views
2

我正在使用Visual Studio客戶端工具在命令行實用程序中調用VSTS REST API。該實用程序可以爲不同的命令(複製,刪除,應用策略等)VssConnection VSTS總是提示輸入憑據

我創建像這樣

public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null) 
{ 
    credentials = credentials ?? new VssClientCredentials();    

    credentials.Storage = new VssClientCredentialStorage();   

    var connection = new VssConnection(url, credentials); 

    connection.ConnectAsync().SyncResult(); 

    return connection; 
} 

根據該文檔,這應該是緩存的VssConnection運行幾次憑據,以便在運行我的命令行工具時不會再提示。但每次運行我的命令行實用程序時都會收到提示,並且VssConnection會嘗試連接。

有沒有辦法緩存憑據,以便每次運行命令行時都不會提示用戶?

需要注意的是,如果我不處理VssConnection,下次運行它時不會提示。

UPDATE 需要明確的是,作爲該對象連接到VssConnection對象問題沒有緩存VssClientCredentials實例一旦創建了連接。問題是在執行程序之間(即在本地機器上)緩存用戶令牌,以便下次從命令行執行該實用程序時,用戶不必再次輸入他們的憑證。類似於您每次啓動時不必始終登錄到Visual Studio的方式。

+1

你預計他們要記住?您的代碼將在每次調用時調用新的VssClientCredentials()。沒有什麼比通過'CreateConnection'調用保存值。 – AdrianHHH

+0

@AdrianHHH根據文檔,VssClientCredentialStorage就是這樣做的。 – Jim

+0

請參閱此處的示例... https://docs.microsoft.com/en-us/vsts/integrate/concepts/dotnet-client-libraries – Jim

回答

3

因此,我發現了一個工作解決方案,似乎正是我想要的。如果有更好的解決方案,請隨時發佈。

解決方案:由於VssClientCredentials.Storage屬性期望實現IVssCredentialStorage的類,因此我創建了一個類,該類通過從股票VssClientCredentialStorage派生類實現該接口。

然後它重寫檢索和刪除令牌的方法,以根據與令牌一起存儲在註冊表中的過期租約來管理它們。

如果令牌被檢索並具有過期租賃,令牌從存儲中刪除,並返回null和VssConnection類顯示UI強迫用戶輸入他們的憑證。如果令牌未過期,則不提示用戶並使用高速緩存的憑證。

所以,現在我可以做到以下幾點:

  • 在命令行再次通過命令行叫我的工具首次
  • 供應憑據VSTS客戶端提示
  • 運行實用程序沒有被提示!

現在我已經在我的實用程序中構建了一個標準的租期到期,但用戶可以使用命令行選項來更改它。此外,用戶也可以清除緩存的憑據。

關鍵在RemoveToken覆蓋。對基類的調用是將它從註冊表中刪除,所以如果繞過(在我的情況下,如果租約尚未過期),那麼註冊表項將保留。這允許VssConnection使用緩存的憑據,並且不會在每次執行程序時提示用戶!調用代碼的

例子:

public static VssConnection CreateConnection(Uri url, VssCredentials credentials = null, double tokenLeaseInSeconds = VssClientCredentialCachingStorage.DefaultTokenLeaseInSeconds) 
    { 
     credentials = credentials ?? new VssClientCredentials(); 

     credentials.Storage = GetVssClientCredentialStorage(tokenLeaseInSeconds); 

     var connection = new VssConnection(url, credentials); 

     connection.ConnectAsync().SyncResult(); 

     return connection; 
    } 

    private static VssClientCredentialCachingStorage GetVssClientCredentialStorage(double tokenLeaseInSeconds) 
    { 
     return new VssClientCredentialCachingStorage("YourApp", "YourNamespace", tokenLeaseInSeconds); 
    } 

派生存儲類:

/// <summary> 
    /// Class to alter the credential storage behavior to allow the token to be cached between sessions. 
    /// </summary> 
    /// <seealso cref="Microsoft.VisualStudio.Services.Common.IVssCredentialStorage" /> 
    public class VssClientCredentialCachingStorage : VssClientCredentialStorage 
    { 
     #region [Private] 

     private const string __tokenExpirationKey = "ExpirationDateTime"; 
     private double _tokenLeaseInSeconds; 

     #endregion [Private] 

     /// <summary> 
     /// The default token lease in seconds 
     /// </summary> 
     public const double DefaultTokenLeaseInSeconds = 86400;// one day 

     /// <summary> 
     /// Initializes a new instance of the <see cref="VssClientCredentialCachingStorage"/> class. 
     /// </summary> 
     /// <param name="storageKind">Kind of the storage.</param> 
     /// <param name="storageNamespace">The storage namespace.</param> 
     /// <param name="tokenLeaseInSeconds">The token lease in seconds.</param> 
     public VssClientCredentialCachingStorage(string storageKind = "VssApp", string storageNamespace = "VisualStudio", double tokenLeaseInSeconds = DefaultTokenLeaseInSeconds) 
      : base(storageKind, storageNamespace) 
     { 
      this._tokenLeaseInSeconds = tokenLeaseInSeconds; 
     } 

     /// <summary> 
     /// Removes the token. 
     /// </summary> 
     /// <param name="serverUrl">The server URL.</param> 
     /// <param name="token">The token.</param> 
     public override void RemoveToken(Uri serverUrl, IssuedToken token) 
     { 
      this.RemoveToken(serverUrl, token, false); 
     } 

     /// <summary> 
     /// Removes the token. 
     /// </summary> 
     /// <param name="serverUrl">The server URL.</param> 
     /// <param name="token">The token.</param> 
     /// <param name="force">if set to <c>true</c> force the removal of the token.</param> 
     public void RemoveToken(Uri serverUrl, IssuedToken token, bool force) 
     { 
      ////////////////////////////////////////////////////////// 
      // Bypassing this allows the token to be stored in local 
      // cache. Token is removed if lease is expired. 

      if (force || token != null && this.IsTokenExpired(token)) 
       base.RemoveToken(serverUrl, token); 

      ////////////////////////////////////////////////////////// 
     } 

     /// <summary> 
     /// Retrieves the token. 
     /// </summary> 
     /// <param name="serverUrl">The server URL.</param> 
     /// <param name="credentialsType">Type of the credentials.</param> 
     /// <returns>The <see cref="IssuedToken"/></returns> 
     public override IssuedToken RetrieveToken(Uri serverUrl, VssCredentialsType credentialsType) 
     { 
      var token = base.RetrieveToken(serverUrl, credentialsType);    

      if (token != null) 
      { 
       bool expireToken = this.IsTokenExpired(token); 
       if (expireToken) 
       { 
        base.RemoveToken(serverUrl, token); 
        token = null; 
       } 
       else 
       { 
        // if retrieving the token before it is expired, 
        // refresh the lease period. 
        this.RefreshLeaseAndStoreToken(serverUrl, token); 
        token = base.RetrieveToken(serverUrl, credentialsType); 
       } 
      } 

      return token; 
     } 

     /// <summary> 
     /// Stores the token. 
     /// </summary> 
     /// <param name="serverUrl">The server URL.</param> 
     /// <param name="token">The token.</param> 
     public override void StoreToken(Uri serverUrl, IssuedToken token) 
     { 
      this.RefreshLeaseAndStoreToken(serverUrl, token); 
     } 

     /// <summary> 
     /// Clears all tokens. 
     /// </summary> 
     /// <param name="url">The URL.</param> 
     public void ClearAllTokens(Uri url = null) 
     { 
      IEnumerable<VssToken> tokens = this.TokenStorage.RetrieveAll(base.TokenKind).ToList(); 

      if (url != default(Uri)) 
       tokens = tokens.Where(t => StringComparer.InvariantCultureIgnoreCase.Compare(t.Resource, url.ToString().TrimEnd('/')) == 0); 

      foreach(var token in tokens) 
       this.TokenStorage.Remove(token); 
     } 

     private void RefreshLeaseAndStoreToken(Uri serverUrl, IssuedToken token) 
     { 
      if (token.Properties == null) 
       token.Properties = new Dictionary<string, string>(); 

      token.Properties[__tokenExpirationKey] = JsonSerializer.SerializeObject(this.GetNewExpirationDateTime()); 

      base.StoreToken(serverUrl, token); 
     } 

     private DateTime GetNewExpirationDateTime() 
     { 
      var now = DateTime.Now; 

      // Ensure we don't overflow the max DateTime value 
      var lease = Math.Min((DateTime.MaxValue - now.Add(TimeSpan.FromSeconds(1))).TotalSeconds, this._tokenLeaseInSeconds); 

      // ensure we don't have negative leases 
      lease = Math.Max(lease, 0); 

      return now.AddSeconds(lease);    
     } 

     private bool IsTokenExpired(IssuedToken token) 
     { 
      bool expireToken = true; 

      if (token != null && token.Properties.ContainsKey(__tokenExpirationKey)) 
      { 
       string expirationDateTimeJson = token.Properties[__tokenExpirationKey]; 

       try 
       { 
        DateTime expiration = JsonSerializer.DeserializeObject<DateTime>(expirationDateTimeJson); 

        expireToken = DateTime.Compare(DateTime.Now, expiration) >= 0; 
       } 
       catch { } 
      } 

      return expireToken; 
     } 
    }