L’intestazione dell’authorization viene persa per il reindirizzamento

Di seguito è riportato il codice che esegue l’autenticazione, genera l’intestazione Authorization e chiama l’API.

Sfortunatamente, ricevo un errore 401 Unauthorized dopo la richiesta GET sull’API.

Tuttavia, quando catturo il traffico in Fiddler e lo riascolto, la chiamata all’API ha esito positivo e posso vedere il codice di stato 200 OK desiderato.

 [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(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); } 

Quando eseguo questo codice, l’intestazione di authorization viene persa.

Tuttavia, in Fiddler quell’intestazione è passata con successo.

Qualche idea su cosa sto facendo male?

Il motivo per cui stai riscontrando questo comportamento è che è di progettazione .

La maggior parte dei client HTTP (per impostazione predefinita) elimina le intestazioni di authorization quando seguono un reindirizzamento.

Una ragione è la sicurezza. Il client potrebbe essere reindirizzato a un server di terze parti non attendibile, su cui non si desidera divulgare il token di authorization.

Quello che puoi fare è rilevare che il reindirizzamento si è verificato e riemettere la richiesta direttamente nella posizione corretta.

L’API restituisce 401 Unauthorized per indicare che l’intestazione dell’authorization manca (o è incompleta). Supporrò che la stessa API restituisca 403 Forbidden se le informazioni di authorization sono presenti nella richiesta ma sono semplicemente errate (nome utente / password errati).

In tal caso, è ansible rilevare la combinazione di “intestazione di authorization di reindirizzamento / mancante” e inviare nuovamente la richiesta.


Ecco il codice della domanda riscritta per fare ciò:

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

Si noti che è ansible salvare il valore di finalRequestUri e utilizzarlo per richieste future per evitare la richiesta aggiuntiva implicata nel tentativo. Tuttavia, poiché si tratta di un reindirizzamento temporaneo, dovresti probabilmente inviare la richiesta alla posizione originale ogni volta.

Disattiverei il comportamento di reindirizzamento automatico e creerò un hander client che nasconde il codice relativo al reindirizzamento temporaneo. La class HttpClient consente di installare DelegatingHandler da cui è ansible modificare la richiesta di risposta.

 public class TemporaryRedirectHandler : DelegatingHandler { protected override async Task 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 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 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 prop in request.Properties) { clone.Properties.Add(prop); } } private void CloneKeyValuePairs(HttpRequestMessage clone, HttpRequestMessage request) { foreach (KeyValuePair> header in request.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } } } 

Istanzerai HttpClient in questo modo:

 var handler = new TemporaryRedirectHandler() { InnerHandler = new HttpClientHandler() { AllowAutoRedirect = false } }; HttpClient client = new HttpClient(handler); 

Ho avuto un problema simile, ma non proprio lo stesso. Nel mio caso, ho avuto anche il problema di reindirizzamento, ma la sicurezza è implementata con OAuth, che ha anche il problema secondario, ma correlato, che a volte i token scadono.

Per questo motivo, mi piacerebbe essere in grado di configurare un HttpClient per andare automaticamente e aggiornare il token OAuth quando riceve una risposta 401 Unauthorized , indipendentemente dal fatto che ciò avvenga a causa di un reindirizzamento o di una scadenza di un token.

La soluzione pubblicata da Chris O’Neill mostra i passaggi generali da eseguire, ma volevo incorporare tale comportamento all’interno di un object HttpClient , invece di dover circondare tutto il nostro codice HTTP con un controllo imperativo. Abbiamo un sacco di codice esistente che utilizza un object HttpClient condiviso, quindi sarebbe molto più facile refactoring il nostro codice se potessi cambiare il comportamento di tale object.

Quanto segue sembra funzionare. L’ho solo prototipato finora, ma sembra che funzioni. Gran parte della nostra base di codice è in F #, quindi il codice è in 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 

Questa è una piccola class che si occupa di aggiornare l'intestazione di Authorization se riceve una risposta 401 Unauthorized . Si aggiorna utilizzando una funzione refreshAuth , che ha l' unit -> Headers.AuthenticationHeaderValue .

Poiché questo è ancora il codice del prototipo, ho fatto in modo che la chiamata interna di SendAsync una chiamata di blocco, lasciandola quindi come esercizio al lettore per implementarla correttamente utilizzando un stream di lavoro asincrono.

Data una funzione di aggiornamento chiamata refreshAuth , puoi creare un nuovo object HttpClient come questo:

 let client = new HttpClient(new TokenRefresher(refreshAuth, new HttpClientHandler ())) 

La risposta inviata da Chris O'Neill si preoccupa di verificare che il nuovo URL sia ancora considerato sicuro. Ho saltato questa considerazione sulla sicurezza qui, ma dovresti considerare di includere un controllo simile prima di ritentare la richiesta.