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

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

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

Однако, когда я захватываю трафик в 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 этот заголовок передается успешно.

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


person Vadim    schedule 17.02.2015    source источник
comment
Когда происходит перенаправление? Какой HTTP-код вы используете для перенаправления?   -  person tia    schedule 17.02.2015
comment
@tia Я получаю временное перенаправление 307   -  person Vadim    schedule 17.02.2015
comment
Не уверен, что это имеет значение   -  person pixelbadger    schedule 20.02.2015
comment
@pixelbadger похоже та же проблема. Я разочарован, что решения нет. В данный момент делаю именно то, что задал вопрос. В моем приложении я использую https напрямую для обхода перенаправления.   -  person Vadim    schedule 20.02.2015


Ответы (3)


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

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

person Chris O'Neill    schedule 23.02.2015
comment
Ах, вы прекрасно объяснили, только одно. что, если при неправильном токене авторизации он снова отправит вызов. так что двойной вызов для неавторизованных пользователей. - person Khawaja Asim; 08.05.2019
comment
Это кажется серьезным недостатком в работе HttpClient. Очевидно, что он должен удалять конфиденциальные данные из переадресации, но тот факт, что он выполняет переадресацию без уведомления вас, в любом случае является серьезным недостатком безопасности. Что, если вы отправите другой конфиденциальный заголовок с предположением, что HttpClient не просто переправит его на какой-то другой сервер? Но по тому же принципу, должны ли мы писать такой код повтора повсюду? HttpClient нужен метод обратного вызова перенаправления, поэтому у нас есть возможность добавлять / удалять заголовки по мере необходимости. - person Christian Findlay; 27.12.2019
comment
@MelbourneDeveloper - Да, согласен. Он удаляет заголовок, когда сайт перенаправляется с http на https, даже если остальная часть URL совпадает (т.е. http://www.test.com to https://www.test.com. Однако он будет вызывать другие сайты, которым вы не доверяете (?) - person Mike; 05.06.2020
comment
Кто-нибудь знает, в какой версии java было введено это поведение по умолчанию? Похоже, что на 1.8.0_231 он ведет себя так, как вы описали, но на 1.8.0_92 он не удаляет заголовок авторизации после перенаправления. - person Sergei Sirik; 26.02.2021

Я бы отключил автоматическое перенаправление и создал клиентского обработчика, который скрывает код, связанный с временным перенаправлением. Класс 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);
person MvdD    schedule 02.03.2017
comment
Зачем отключать автоматическую переадресацию? - person Mark Seemann; 03.03.2017
comment
@MarkSeemann Так что я могу справиться с ними сам в устанавливаемом мной обработчике клиента. - person MvdD; 03.03.2017
comment
Привет, @MvdD, ты ведь случайно не завернул свой класс в пакет NuGet, не так ли? Не возражаете, если я это сделаю? Мне нужно немного его настроить (я хочу убедиться, что имена хостов исходного запроса и перенаправления совпадают в моем варианте использования), а затем сделать его доступным для моих клиентов. - person Michael Welch; 23.10.2018
comment
Большое спасибо за это, мне пришлось изменить оператор if в ответе SendAsync.StatusCode == HttpStatusCode.TemporaryRedirect || response.StatusCode == HttpStatusCode.Found, но в остальном работал отлично - person Jeanno; 30.03.2020

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

person Mark Seemann    schedule 02.03.2017
comment
Однако это решает другую проблему (обновление токена). - person MvdD; 03.03.2017
comment
@MvdD Это зависит от того, как вы реализуете refreshAuth. - person Mark Seemann; 03.03.2017
comment
Конечно, но обычно вы не хотите обновлять токен, если срок его действия не истек. В случае OP срок действия токена не истек, но он был исключен из перенаправленного запроса. - person MvdD; 03.03.2017
comment
Кроме того, вы по-прежнему вызываете перенаправленный адрес, который не работает из-за отсутствия заголовка Authorization. Кажется расточительным. - person MvdD; 03.03.2017
comment
Другая потенциальная проблема заключается в том, что HttpRequestMessage нельзя использовать повторно. Можно ли использовать response.RequestMessage? - person Foole; 03.03.2017
comment
@Foole Это работает в специальных тестах, которые я провел до сих пор ... но возможно, что клонирование сообщения окажется необходимым. - person Mark Seemann; 03.03.2017