以下は、認証を行い、Authorizationヘッダーを生成し、APIを呼び出すコードです。
残念ながら、401 Unauthorized
APIのGET
リクエストに続くエラー。
ただし、Fiddlerでトラフィックをキャプチャして再生すると、APIの呼び出しは成功し、目的の200 OK
ステータスコード。
[Test]
public void RedirectTest()
{
HttpResponseMessage response;
var client = new HttpClient();
using (var authString = new StringContent(@"{username: ""theUser"", password: ""password""}", Encoding.UTF8, "application/json"))
{
response = client.PostAsync("http://Host/api/authenticate", authString).Result;
}
string result = response.Content.ReadAsStringAsync().Result;
var authorization = JsonConvert.DeserializeObject<CustomAutorization>(result);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorization.Scheme, authorization.Token);
client.DefaultRequestHeaders.Add("Accept", "application/vnd.Host+json;version=1");
response =
client.GetAsync("http://Host/api/getSomething").Result;
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
このコードを実行すると、Authorizationヘッダーが失われます。
ただし、Fiddlerでは、そのヘッダーは正常に渡されます。
私が間違っていることは何ですか?
この動作が発生する理由は、設計によるであるためです。
ほとんどのHTTPクライアント(デフォルト)は、リダイレクトを追跡するときに認証ヘッダーを取り除きます。
1つの理由はセキュリティです。クライアントは、信頼されていない第三者サーバーにリダイレクトされる可能性があります。第三者サーバーは、認証トークンを公開したくないサーバーです。
できることは、リダイレクトが発生したことを検出し、正しい場所に直接リクエストを再発行することです。
APIが401 Unauthorized
を返し、認証ヘッダーが欠落している(または不完全である)ことを示しています。承認情報がリクエストに存在するが、単に間違っている(間違ったユーザー名/パスワード)場合、同じAPIが403 Forbidden
を返すと仮定します。
この場合、「リダイレクト/欠落した認証ヘッダー」の組み合わせを検出し、リクエストを再送信できます。
これを行うために書き直された質問のコードは次のとおりです。
[Test]
public void RedirectTest()
{
// These lines are not relevant to the problem, but are included for completeness.
HttpResponseMessage response;
var client = new HttpClient();
using (var authString = new StringContent(@"{username: ""theUser"", password: ""password""}", Encoding.UTF8, "application/json"))
{
response = client.PostAsync("http://Host/api/authenticate", authString).Result;
}
string result = response.Content.ReadAsStringAsync().Result;
var authorization = JsonConvert.DeserializeObject<CustomAutorization>(result);
// Relevant from this point on.
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorization.Scheme, authorization.Token);
client.DefaultRequestHeaders.Add("Accept", "application/vnd.Host+json;version=1");
var requestUri = new Uri("http://Host/api/getSomething");
response = client.GetAsync(requestUri).Result;
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Authorization header has been set, but the server reports that it is missing.
// It was probably stripped out due to a redirect.
var finalRequestUri = response.RequestMessage.RequestUri; // contains the final location after following the redirect.
if (finalRequestUri != requestUri) // detect that a redirect actually did occur.
{
if (IsHostTrusted(finalRequestUri)) // check that we can trust the Host we were redirected to.
{
response = client.GetAsync(finalRequestUri).Result; // Reissue the request. The DefaultRequestHeaders configured on the client will be used, so we don't have to set them again.
}
}
}
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
private bool IsHostTrusted(Uri uri)
{
// Do whatever checks you need to do here
// to make sure that the Host
// is trusted and you are happy to send it
// your authorization token.
if (uri.Host == "Host")
{
return true;
}
return false;
}
finalRequestUri
の値を保存し、今後の要求に使用して、再試行に伴う余分な要求を回避できることに注意してください。ただし、これは一時的なリダイレクトなので、おそらく毎回元の場所にリクエストを発行する必要があります。
自動リダイレクト動作をオフにして、一時リダイレクトを処理するコードを隠すクライアントハンドラを作成します。 HttpClient
クラスを使用すると、DelegatingHandler
sをインストールして、そこから応答要求を変更できます。
public class TemporaryRedirectHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.TemporaryRedirect)
{
var location = response.Headers.Location;
if (location == null)
{
return response;
}
using (var clone = await CloneRequest(request, location))
{
response = await base.SendAsync(clone, cancellationToken);
}
}
return response;
}
private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage request, Uri location)
{
var clone = new HttpRequestMessage(request.Method, location);
if (request.Content != null)
{
clone.Content = await CloneContent(request);
if (request.Content.Headers != null)
{
CloneHeaders(clone, request);
}
}
clone.Version = request.Version;
CloneProperties(clone, request);
CloneKeyValuePairs(clone, request);
return clone;
}
private async Task<StreamContent> CloneContent(HttpRequestMessage request)
{
var memstrm = new MemoryStream();
await request.Content.CopyToAsync(memstrm).ConfigureAwait(false);
memstrm.Position = 0;
return new StreamContent(memstrm);
}
private void CloneHeaders(HttpRequestMessage clone, HttpRequestMessage request)
{
foreach (var header in request.Content.Headers)
{
clone.Content.Headers.Add(header.Key, header.Value);
}
}
private void CloneProperties(HttpRequestMessage clone, HttpRequestMessage request)
{
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
}
private void CloneKeyValuePairs(HttpRequestMessage clone, HttpRequestMessage request)
{
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
}
次のようにHttpClientをインスタンス化します。
var handler = new TemporaryRedirectHandler()
{
InnerHandler = new HttpClientHandler()
{
AllowAutoRedirect = false
}
};
HttpClient client = new HttpClient(handler);
同様の問題がありましたが、まったく同じではありませんでした。私の場合、リダイレクトの問題もありましたが、セキュリティはOAuthで実装されており、OAuthにはトークンが期限切れになるという二次的な問題があります。
そのため、HttpClient
を設定して、401 Unauthorized
応答を受信したときに、OAuthトークンを自動的に移動して更新するように設定したいこれがリダイレクトによるものか、トークンの有効期限によるものか。
Chris O'Neillによって投稿されたソリューションは、実行する一般的な手順を示していますが、すべてのHTTPコードを命令チェックで囲む必要はなく、HttpClient
オブジェクト内にその動作を埋め込みたいと思いました。共有HttpClient
オブジェクトを使用する既存のコードが多数あるため、そのオブジェクトの動作を変更できれば、コードをリファクタリングするのがはるかに簡単になります。
以下は動作しているようです。これまでプロトタイプを作成しただけですが、機能しているようです。コードベースの多くはF#にあるため、コードはF#にあります。
open System.Net
open System.Net.Http
type TokenRefresher (refreshAuth, inner) =
inherit MessageProcessingHandler (inner)
override __.ProcessRequest (request, _) = request
override __.ProcessResponse (response, cancellationToken) =
if response.StatusCode <> HttpStatusCode.Unauthorized
then response
else
response.RequestMessage.Headers.Authorization <- refreshAuth ()
inner.SendAsync(response.RequestMessage, cancellationToken).Result
これは、401 Unauthorized
応答を受信した場合にAuthorization
ヘッダーを更新する小さなクラスです。 unit -> Headers.AuthenticationHeaderValue
型のrefreshAuth
関数を挿入して更新します。
これはまだプロトタイプコードであるため、内側のSendAsync
呼び出しをブロッキング呼び出しにして、非同期ワークフローを使用して適切に実装するための演習として読者に任せました。
refreshAuth
と呼ばれる更新関数を指定すると、次のように新しいHttpClient
オブジェクトを作成できます。
let client = new HttpClient(new TokenRefresher(refreshAuth, new HttpClientHandler ()))
Chris O'Neillが投稿した回答は、新しいURLがまだ安全であると見なされることを確認するために注意を払います。ここではそのセキュリティに関する考慮事項をスキップしましたが、リクエストを再試行する前に同様のチェックを含めることを強く検討する必要があります。