2017-11-10 417 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; 
     } 
    }