2017-05-29 96 views
0

我试图使用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。 这是一个更好的方法吗?还是我只是挖掘到一个更大的洞?

+0

我发现有点奇怪,你可以有20个不同的令牌提供者,每个都有自己的客户端,他们都可以返回相同的标记。这不会让你深深伤心吗?令牌提供者是否应该提供给定客户端的令牌*? –

+0

你是对的!我已经更改了代码,使_token和_lock现在是实例字段。 – SabrinaMH

+0

然后我确定,在引导应用程序时,我只有一个TokenProvider实例。 – SabrinaMH

回答

2

但是,我有我的怀疑,以上是要走的路。这种怀疑主要基于编译器优化;看到这篇文章由埃里克Lippert

我们不必假设异国情调的编译器优化。首先,有明显的问题,即是否令牌过期变化

  • 没有在静态变量的未过期的令牌。
  • 线程检测到存在未过期的令牌并输入if
  • 线程A挂起。
  • 线程B运行的时间足够长,令牌过期。
  • 线程恢复并返回过期的令牌。

所以就是这样。我们不保证返回的令牌有效。但比这更糟糕。我们甚至没有保证返回的令牌是当前令牌。

  • 静态变量中有一个未过期的标记。
  • 线程检测到存在未过期的令牌并输入if
  • 线程A将未过期的标记放在评估栈上作为返回。
  • 线程A挂起。
  • 线程B运行的时间足够长,令牌过期。
  • 线程C运行,检测到令牌已过期,并用不同的令牌替换它。
  • 线程恢复并返回一个甚至不是变量当前内容的过期令牌。

这里有个TOCTOU问题,不管你实现双重检查锁定有什么问题。那是检查时间不是使用时间。你所知道的令牌是它在过去的的某个时间没有过期,但所有令牌都是如此。

+0

非常感谢非常有帮助的评论 - 让我意识到我面临的问题的正式名称。 我很困惑,我似乎无法找到任何人描述如何处理具体问题(我想很多人会想重用令牌服务来通信服务)。 你有没有机会知道任何资源? – SabrinaMH