2017-11-10 1 views
2

Visual Studio 클라이언트 도구를 사용하여 명령 줄 유틸리티에서 VSTS REST API를 호출하고 있습니다. 이 유틸리티는 다른 명령 (복사, 정책 적용 등을 삭제)VSTS에 대한 VssConnection은 항상 자격 증명을 묻습니다.

나는 그런

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; 
     } 
    } 
관련 문제