【问题标题】:Reuse token for service to service communication重用令牌进行服务到服务的通信
【发布时间】:2017-05-29 18:25:28
【问题描述】:

我正在尝试使用 OAuth(从现在开始称为消费者和提供者)保护两个服务之间的通信。

让我们假设消费者刚刚启动。现在多个 http 调用几乎同时到达它。消费者需要与提供者通信才能处理请求。我非常希望消费者为这种通信重用一个令牌(而不是为每个传入的请求获取一个新令牌)。首先,当令牌过期时,应该获取一个新令牌。

如何做到这一点?

   public class TokenProvider
   {
        private readonly HttpClient _httpClient;
        private Token _token;
        private object _lock = new object();

        public TokenProvider(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<string> GetTokenAsync()
        {
            if (_token != null && !_token.IsExpired())
            {
                return _token;
            }
            else
            {
                string oauthPostBody = string.Format(
                    "grant_type=client_credentials&client_id={0}&client_secret={1}", "fakeClientId", "fakeSecret");
                var tokenEndpoint = ...;
                var response = await _httpClient.PostAsync(tokenEndpoint.Uri, new StringContent(oauthPostBody));
                var responseContent = await response.Content.ReadAsStringAsync();
                var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);

                lock (_lock)
                {
                    if (_token == null || _token.IsExpired())
                    {
                        string expiresIn = jsonResponse.expires_in;
                        _token = new Token(jsonResponse.access_token, int.Parse(expiresIn));
                    }
                    return _token;
                }
            }
        }

        private class Token
        {
            private readonly string _token;
            private readonly DateTime _expirationDateTime;

            public Token(string token, int expiresIn)
            {
                _token = token;
                _expirationDateTime = DateTime.UtcNow.AddSeconds(expiresIn);
            }

            public bool IsExpired()
            {
                return DateTime.UtcNow > _expirationDateTime;
            }

            public static implicit operator string(Token token)
            {
                return token._token;
            }
        }
    }

但是,我怀疑上述方法是否可行。这种怀疑是基于编译器优化等。请参阅 Eric Lippert 的 this post。

我正在尝试让多个线程同时读取令牌,但只能由单个线程更新。我也研究了 ReaderWriterLockSlim,但这似乎无助于解决我的问题。 (请注意,由于我在 GetTokenAsync 中有一个异步调用,因此它变得更加复杂。)

更新 根据@EricLippert 的评论,我更新了代码:

public class TokenProvider
{
    private readonly HttpClient _httpClient;
    private readonly IApplicationConfig _config;
    private Token _token;
    private AsyncReaderWriterLock _lock = new AsyncReaderWriterLock();

    public TokenProvider(HttpClient httpClient, IApplicationConfig config)
    {
        _httpClient = httpClient;
        _config = config;
    }

    public bool TryGetExistingToken(out string token)
    {
        using (_lock.ReaderLock())
        {
            if (_token != null)
            {
                token = _token;
                return true;
            }
            else
            {
                token = null;
                return false;
            }
        }
    }

    public async Task<string> GetNewTokenAsync()
    {
        using (await _lock.WriterLockAsync())
        {
            if (_token != null && !_token.IsExpired())
            {
                return _token;
            }
            else
            {
                var clientId = _config.Get<string>("oauth.clientId");
                var secret = _config.Get<string>("oauth.sharedSecret");
                string oauthPostBody = string.Format(
                    "grant_type=client_credentials&client_id={0}&client_secret={1}", clientId, secret);
                var queueEndpoint = _config.GetUri("recommendationQueue.host");
                var tokenPath = _config.Get<string>("recommendationQueue.path.token");
                var tokenEndpoint = new UriBuilder(queueEndpoint) {Path = tokenPath};
                var response = await _httpClient.PostAsync(tokenEndpoint.Uri, new StringContent(oauthPostBody));
                var responseContent = await response.Content.ReadAsStringAsync();
                var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);

                if (_token == null || _token.IsExpired())
                {
                    string expiresIn = jsonResponse.expires_in;
                    string accessToken = jsonResponse.access_token;
                    _token = new Token(accessToken, int.Parse(expiresIn));
                }
                return _token;
            }
        }
    }

    private class Token
    {
        private readonly string _token;
        private readonly DateTime _expirationDateTime;

        public Token(string token, int expiresIn)
        {
            _token = token;
            _expirationDateTime = DateTime.UtcNow.AddSeconds(expiresIn);
        }

        public bool IsExpired()
        {
            return DateTime.UtcNow > _expirationDateTime;
        }

        public static implicit operator string(Token token)
        {
            return token._token;
        }
    }
}

我正在使用 Stephen Cleary 的 AsyncReaderWriterLock。 这是更好的方法吗?还是我只是把自己挖到了一个更大的坑里?

【问题讨论】:

  • 我觉得有点奇怪的是,您可以拥有 20 个不同的令牌提供者,每个提供者都有自己的客户端,并且它们都可以返回相同的令牌。这不是让你感到深深的破碎吗?令牌提供者不应该提供一个令牌来自给定客户端 ???
  • 你完全正确!我已经更改了代码,使得 _token 和 _lock 现在是实例字段。
  • 然后我确保在引导应用程序时,我只有一个 TokenProvider 实例。
  • 我不明白的是,这些方法中的任何一种保证是什么。显然不是返回了有效的令牌。你能说的最好的就是将努力返回一个有效的令牌。令牌是如何使用的?您是否在循环中尝试获取有效令牌并在检查时间和使用时间之间证明它无效时重试?
  • 另外,请考虑以下场景。线程 A 获取锁 X。我们切换到线程 B。线程 B 获取写入器锁,然后在持有锁的同时等待。所以我们回到试图做其他工作的调用者。该工作导致线程 B 尝试获取锁 X,因此 B 阻塞,我们切换回线程 A。现在线程 A 尝试获取写入器锁并阻塞。两个线程现在都被阻塞了,锁永远不会被释放。是什么阻止了您的程序中出现这种情况?

标签: .net multithreading oauth locking token


【解决方案1】:

但是,我怀疑上述方法是否可行。这种怀疑是基于编译器优化等。请参阅 Eric Lippert 的这篇文章

我们不必假设奇异的编译器优化。首先,一个明显的问题是令牌是否过期变化:

  • 静态变量中有未过期的令牌。
  • 线程A检测到有未过期的token,进入if。
  • 线程 A 挂起。
  • 线程 B 运行的时间足够长,以至于令牌过期。
  • 线程 A 恢复并返回一个过期的令牌。

就是这样。我们不保证返回的令牌是有效的。但情况远比这糟糕。我们甚至不能保证返回的令牌是当前令牌。

  • 静态变量中有未过期的令牌。
  • 线程A检测到有未过期的token,进入if。
  • 线程 A 将未过期的令牌放入评估堆栈以供返回。
  • 线程 A 挂起。
  • 线程 B 运行的时间足够长,以至于令牌过期。
  • 线程 C 运行,检测到令牌已过期,并将其替换为不同的令牌。
  • 线程 A 恢复并返回一个过期的令牌,该令牌甚至不是变量的当前内容。

您在此处遇到了 TOCTOU 问题,无论您的双重检查锁定的实现是否存在任何问题。那就是 Time-Of-Check-is-not-Time-Of-Use。您对令牌的所有了解是它在过去的某个时间没有过期,但所有令牌都是如此。

【讨论】:

  • 非常感谢您提供的非常有帮助的评论 - 并让我意识到我所面临的问题的正式名称。我很困惑,我似乎找不到任何人描述如何处理具体问题(我猜很多人希望重用令牌进行服务到服务的通信)。你有没有机会知道任何资源?
猜你喜欢
  • 2021-02-15
  • 1970-01-01
  • 2014-03-18
  • 1970-01-01
  • 1970-01-01
  • 2021-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多