【问题标题】:Authorization header is lost on redirect重定向时授权标头丢失
【发布时间】:2015-02-17 15:28:00
【问题描述】:

以下是执行身份验证、生成授权标头和调用 API 的代码。

不幸的是,在 API 上的 GET 请求之后,我收到了 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 中,该标头已成功传递。

知道我做错了什么吗?

【问题讨论】:

  • 什么时候发生重定向?您使用哪个 HTTP 代码进行重定向?
  • @tia 我得到 307 临时重定向
  • @pixelbadger 它看起来像同样的问题。我很失望没有解决方案。目前我正在做的正是问这个问题的人。在我的应用程序中,我直接使用 https 来绕过重定向。

标签: c# .net rest dotnet-httpclient


【解决方案1】:

您遇到此行为的原因是设计使然

大多数 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 的值并将其用于将来的请求,以避免重试中涉及的额外请求。但是,由于这是一个临时重定向,您可能应该每次都向原始位置发出请求。

【讨论】:

  • 啊,你解释得很漂亮,只有一件事。如果授权令牌错误时它会再次发送调用怎么办。所以双重呼吁未经授权的用户。
  • 这似乎是 HttpClient 工作方式中的一个巨大缺陷。显然它应该从重定向中去除敏感数据,但它在不告诉你的情况下重定向的事实无论如何都是一个巨大的安全漏洞。如果您假设 HttpClient 不会将其发送到其他服务器,则发送一些其他敏感标头怎么办?但是,出于同样的原因,我们是否应该到处写这样的重试代码? HttpClient 需要一个重定向回调方法,因此我们有机会根据需要添加/删除标头。
  • @MelbourneDeveloper - 是的,我同意。当站点从 http 重定向到 https 时,它会去除标头,即使其余 url 匹配(即http://www.test.com to https://www.test.com。但是,它会调用您不信任的其他站点(?)
  • 有人知道这个默认行为是在哪个Java版本中引入的吗?似乎在 1.8.0_231 上它的行为就像你描述的那样,但在 1.8.0_92 上它不会在重定向后丢弃 Authorization 标头。
【解决方案2】:

我会关闭自动重定向行为并创建一个隐藏处理临时重定向的代码的客户端处理程序。 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);

【讨论】:

  • 为什么要关闭自动重定向?
  • @MarkSeemann 所以我可以在我安装的客户端处理程序中自己处理它们。
  • 嘿@MvdD,你没有碰巧把你的课程包在一个 NuGet 包中吧?介意我吗?我需要稍微调整一下(我想确保原始请求的主机名和我的用例中的重定向匹配),然后将其提供给我的客户。
  • 非常感谢,我不得不更改 SendAsync response.StatusCode == HttpStatusCode.TemporaryRedirect || 中的 if 语句response.StatusCode == HttpStatusCode.Found ,否则效果很好
【解决方案3】:

我有类似的问题,但不完全相同。在我的情况下,我也遇到了重定向问题,但安全性是通过 OAuth 实现的,它还有次要但相关的问题,即令牌有时会过期。

因此,我希望能够配置 HttpClient 以在收到 401 Unauthorized 响应时自动刷新 OAuth 令牌,无论这是由于重定向还是令牌而发生的过期。

Chris O'Neill 发布的解决方案显示了要采取的一般步骤,但我想将该行为嵌入到 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

这是一个小类,负责在收到401 Unauthorized 响应时刷新Authorization 标头。它使用注入的refreshAuth 函数进行刷新,该函数的类型为unit -&gt; Headers.AuthenticationHeaderValue

由于这仍然是原型代码,我将内部 SendAsync 调用设置为阻塞调用,从而将其作为练习留给读者使用异步工作流正确实现它。

给定一个名为 refreshAuth 的刷新函数,您可以像这样创建一个新的 HttpClient 对象:

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

Chris O'Neill 发布的答案会仔细检查新 URL 是否仍被认为是安全的。我在这里跳过了安全考虑,但您应该强烈考虑在重试请求之前包含类似的检查。

【讨论】:

  • 这解决了一个不同的问题(令牌刷新)。
  • @MvdD 这取决于你如何实现refreshAuth
  • 当然,但通常您不想刷新令牌,除非它已过期。在 OP 情况下,令牌没有过期,但从重定向请求中省略。
  • 另外,由于缺少 Authorization 标头,您仍在对重定向的位置进行调用,该位置将失败。看起来很浪费。
  • @Foole 它适用于我迄今为止所做的临时测试......但克隆消息可能是必要的。
猜你喜欢
  • 2021-02-14
  • 2014-06-16
  • 2016-04-27
  • 1970-01-01
  • 2017-06-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多