2017-05-29 88 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