0

我有一个帮助类来从 IdentityServer4 获取访问令牌。这是代码:

public class ServerTokenHelper
{
    static TokenResponse Token { get; set; }
    static DateTime ExpiryTime { get; set; }

    string  _host;
    string _clientId;
    string _clientSecret;
    string _clientScopes;

    static object ThreadLock = new object();


    static ConcurrentDictionary<string, Tuple<string, string, TokenResponse, DateTime>> userNameCache =
        new ConcurrentDictionary<string, Tuple<string, string, TokenResponse, DateTime>>();
    private static HttpClient _tokenClient = new HttpClient();

    public ServerTokenHelper(string commAddress, string host, string clientId, string clientSecret, string clientScopes)
    {
        _host = host;
        _clientId = clientId;
        _clientSecret = clientSecret;
        _clientScopes = clientScopes;
    }
        public async Task<TokenResponse> GetUserTokenResponseAsync(string userName, string password)
    {
        if (userName != null && userName.Length > 0)
        {
            lock (ThreadLock)
            {
                if (userNameCache.TryGetValue(userName, out var cacheItem))
                {
                    // Since we always cache the result below, we should verify before reusing an entry that the IdentityToken
                    // isn't null because of an error getting it last time!
                    if (cacheItem.Item2 == password && cacheItem.Item3 != null && cacheItem.Item3.IdentityToken != null
                        && cacheItem.Item4 > DateTime.UtcNow)
                    {
                        // System.Diagnostics.Debug.WriteLine($"GetUserTokenResponseAsync({userName}): returning cached value");
                        return cacheItem.Item3;
                    }
                }
            }
        }

        Trace.WriteLine($"GetUserTokenResponseAsync({userName}): new token being retrieved...");

        bool blHttps = false;
        if (_host.ToLower().Contains("https")) blHttps = true;
        var disco = await _tokenClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
        {
            Address = _host,
            Policy = { RequireHttps = blHttps }
        });
        if (disco.IsError)
        {
            Trace.WriteLine($"GetUserTokenResponseAsync({userName}): GetDiscoveryDocumentAsync failed: {disco.Error}");
            return null;
        }
        // request token
        var tokenResponse = await _tokenClient.RequestPasswordTokenAsync(new PasswordTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = _clientId,
            ClientSecret = _clientSecret,
            Scope = _clientScopes,
            UserName = userName,
            Password = password
        });
        if (tokenResponse.IsError)
        {
            Trace.WriteLine($"GetUserTokenResponseAsync({userName}): Could not retrieve token. {tokenResponse.Error} - {tokenResponse.ErrorDescription}");
        }

        lock (ThreadLock)
        {
            userNameCache[userName] = Tuple.Create(userName, password, tokenResponse,
                DateTime.UtcNow.AddSeconds((tokenResponse != null) ? tokenResponse.ExpiresIn - 120 : 0));
        }

        return tokenResponse;
    }

上面代码的目的是获取用户资源密码流的访问令牌。最近,有人从

private HttpClient _tokenClient = new HttpClient();

private static HttpClient _tokenClient = new HttpClient();

通过此更改,我们偶尔会遇到一些错误。代码功能在生产服务器中。每小时可能有数千个 api 调用。这是错误消息:

GetUserTokenResponseAsync: GetDiscoveryDocumentAsync failed

有人可以解释这个简单的变化引起的问题是什么吗?

4

1 回答 1

2

使用 HttpClient 这条线是问题所在

private static HttpClient _tokenClient = new HttpClient();

HttpClient 不应该被重用/缓存,而是应该在每次使用后处理它,因为否则您可能会收到各种问题,例如 DNS 或 TCP/IP 端口用完。

但更好的是,为什么不将发现文档缓存 X 分钟呢?该文件不会经常更改。

请参阅这些文章:

于 2020-08-26T06:38:30.380 回答