VssConnection 到 VSTS 总是提示输入凭据
VssConnection to VSTS always prompts for credentials
我正在使用 Visual Studio 客户端工具在命令行实用程序中调用 VSTS REST API。对于不同的命令(复制、删除、应用策略等),此实用程序可以 运行 多次
我正在这样创建 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,下次我运行它就不会提示了。
更新
需要明确的是,问题不在于在创建连接后缓存 VssClientCredentials 实例,因为该对象附加到 VssConnection 对象。问题是在程序执行之间缓存用户令牌,即在本地机器上,以便下次从命令行执行该实用程序时,用户不必再次输入他们的凭据。类似于您不必每次启动时都登录 Visual Studio。
所以我找到了一个似乎正是我想要的可行解决方案。如果有更好的解决办法,欢迎留言post.
解决方案:由于 VssClientCredentials.Storage
属性 期望实现 IVssCredentialStorage
的 class,我创建了一个 class 通过从库存 VssClientCredentialStorage
class.
然后它会覆盖围绕检索和删除令牌的方法,以根据与令牌一起存储在注册表中的到期租约来管理它们。
如果检索到令牌并且租约已过期,则令牌将从存储中删除并返回 null 并且 VssConnection
class 显示 UI 强制用户输入他们的凭据。如果令牌未过期,则不会提示用户并使用缓存的凭据。
所以现在我可以执行以下操作:
- 第一次从命令行调用我的实用程序
- 向 VSTS 客户端提示提供凭据
- 运行 从命令行再次使用该实用程序而不提示!
现在我在我的实用程序中内置了一个标准的租约到期时间,但用户可以使用命令行选项更改它。用户也可以清除缓存的凭据。
关键在于 RemoveToken 覆盖。对基础 class 的调用是将其从注册表中删除的原因,因此如果您绕过它(在我的情况下,如果租约尚未到期),则注册表项将保留。这允许 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);
}
导出存储class:
/// <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;
}
}
我正在使用 Visual Studio 客户端工具在命令行实用程序中调用 VSTS REST API。对于不同的命令(复制、删除、应用策略等),此实用程序可以 运行 多次
我正在这样创建 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,下次我运行它就不会提示了。
更新 需要明确的是,问题不在于在创建连接后缓存 VssClientCredentials 实例,因为该对象附加到 VssConnection 对象。问题是在程序执行之间缓存用户令牌,即在本地机器上,以便下次从命令行执行该实用程序时,用户不必再次输入他们的凭据。类似于您不必每次启动时都登录 Visual Studio。
所以我找到了一个似乎正是我想要的可行解决方案。如果有更好的解决办法,欢迎留言post.
解决方案:由于 VssClientCredentials.Storage
属性 期望实现 IVssCredentialStorage
的 class,我创建了一个 class 通过从库存 VssClientCredentialStorage
class.
然后它会覆盖围绕检索和删除令牌的方法,以根据与令牌一起存储在注册表中的到期租约来管理它们。
如果检索到令牌并且租约已过期,则令牌将从存储中删除并返回 null 并且 VssConnection
class 显示 UI 强制用户输入他们的凭据。如果令牌未过期,则不会提示用户并使用缓存的凭据。
所以现在我可以执行以下操作:
- 第一次从命令行调用我的实用程序
- 向 VSTS 客户端提示提供凭据
- 运行 从命令行再次使用该实用程序而不提示!
现在我在我的实用程序中内置了一个标准的租约到期时间,但用户可以使用命令行选项更改它。用户也可以清除缓存的凭据。
关键在于 RemoveToken 覆盖。对基础 class 的调用是将其从注册表中删除的原因,因此如果您绕过它(在我的情况下,如果租约尚未到期),则注册表项将保留。这允许 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);
}
导出存储class:
/// <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;
}
}