重新使用令牌进行服务通信

时间:2017-05-29 18:25:29

标签: .net multithreading oauth locking token

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

让我们想象消费者刚刚起步。现在,多个http呼叫几乎同时到达。消费者需要与提供者通信以处理请求。我非常希望消费者为此通信重用单个令牌(而不是为每个传入请求获取新令牌)。首先,当令牌过期时,应该获取一个新令牌。

如何实现这一目标?

$ python3 bst.py
5
8
10
15

然而,我怀疑以上是要走的路。这种怀疑基于编译器优化等;见Eric Lippert的this post

我正在尝试这样做,令牌可以被许多线程一次读取,但只能由一个更新。我也研究过ReaderWriterLockSlim,但这似乎没有帮助解决我的问题。 (请注意,由于我在GetTokenAsync中进行了异步调用,因此更加复杂。)

更新 基于@EricLippert的评论,我更新了代码:

   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;
            }
        }
    }

我正在使用Stephen Cleary的AsyncReaderWriterLock。 这是一种更好的方法吗?或者我只是把自己挖进一个更大的洞?

1 个答案:

答案 0 :(得分:2)

  

然而,我怀疑以上是要走的路。这种怀疑基于编译器优化等;看Eric Lippert的这篇文章

我们不必假设奇特的编译器优化。首先,明显的问题是令牌是否已过期更改

  • 静态变量中有一个未到期的令牌。
  • 线程A检测到有未到期的令牌并进入if
  • 主题A暂停。
  • 线程B运行的时间足以让令牌过期。
  • 线程A恢复并返回过期的令牌。

所以就是这样。我们无法保证返回的令牌有效。但它远不止于此。我们甚至不保证返回的令牌是当前令牌。

  • 静态变量中有一个未到期的令牌。
  • 线程A检测到有未到期的令牌并进入if
  • 线程A将未到期的令牌放在评估堆栈上以供返回。
  • 主题A暂停。
  • 线程B运行的时间足以让令牌过期。
  • 线程C运行,检测到令牌已过期,并用其他令牌替换它。
  • 线程A恢复并返回一个过期的令牌,该令牌甚至不是变量的当前内容。

此处存在TOCTOU问题,无论您实施双重检查锁定是否存在任何问题。那是检查时间不是使用时间。你对令牌的所有了解都是它在过去的某个时间没有过期,但所有令牌都是如此。