it-swarm.com.ru

Заголовок авторизации теряется при перенаправлении

Ниже приведен код, который выполняет аутентификацию, генерирует заголовок авторизации и вызывает API.

К сожалению, я получаю ошибку 401 Unauthorized после запроса GET в API. 

Однако, когда я собираю трафик в 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);
}

Когда я запускаю этот код, заголовок авторизации теряется.

Однако в Fiddler этот заголовок успешно передан.

Есть идеи, что я делаю не так?

14
Vadim

Причина, по которой вы испытываете такое поведение, заключается в том, что оно по замыслу

Большинство клиентов HTTP (по умолчанию) удаляют заголовки авторизации при перенаправлении.

Одна из причин - безопасность. Клиент может быть перенаправлен на ненадежный сторонний сервер, на который вы не захотите раскрывать свой токен авторизации.

Что вы можете сделать, это обнаружить, что перенаправление произошло, и повторно отправить запрос в правильное местоположение.

Ваш 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 и использовать его для будущих запросов, чтобы избежать дополнительного запроса, связанного с повторной попыткой. Однако, поскольку это временное перенаправление, вам, вероятно, следует каждый раз отправлять запрос в исходное местоположение.

42
Chris O'Neill

Я бы отключил автоматическое поведение перенаправления и создал бы клиентскую утилиту, которая скрывает код, связанный с временным перенаправлением. Класс HttpClient позволяет вам установить DelegatingHandlers, из которого вы можете изменить запрос ответа.

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);
2
MvdD

У меня была похожая проблема, но не совсем такая же. В моем случае у меня также была проблема с перенаправлением, но безопасность реализована с помощью OAuth, который также имеет вторичную, но связанную проблему, у которой токены иногда устаревают.

По этой причине я хотел бы иметь возможность настроить HttpClient для автоматического запуска и обновления токена OAuth при получении ответа 401 Unauthorized независимо от того, происходит ли это из-за перенаправления или истечения срока действия токена.

Решение, опубликованное Крисом О'Нилом, показывает общие шаги, которые нужно предпринять, но я хотел встроить это поведение в объект HttpClient вместо того, чтобы окружать весь наш HTTP-код обязательной проверкой. У нас есть много существующего кода, который использует общий объект 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

Это небольшой класс, который заботится об обновлении заголовка Authorization, если он получает ответ 401 Unauthorized. Он обновляется с использованием встроенной функции refreshAuth, которая имеет тип unit -> Headers.AuthenticationHeaderValue.

Поскольку это все еще прототипный код, я сделал внутренний вызов SendAsync блокирующим вызовом, оставив читателю в качестве упражнения правильную реализацию с использованием асинхронного рабочего процесса.

Имея функцию обновления с именем refreshAuth, вы можете создать новый объект HttpClient, например:

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

Ответ, опубликованный Крисом О'Нилом, заботится о том, чтобы убедиться, что новый URL все еще считается безопасным. Я пропустил это соображение безопасности здесь, но вы должны настоятельно рассмотреть возможность включения аналогичной проверки, прежде чем повторять запрос.

0
Mark Seemann