授权标头在重定向时丢失

下面是进行身份validation的代码,生成Authorization标头并调用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(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客户端(默认情况下)会删除授权标头。

一个原因是安全。 客户端可能会被重定向到不受信任的第三方服务器,您不希望向其披露您的授权令牌。

您可以做的是检测重定向是否已发生,并将请求直接重新发送到正确的位置。

您的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(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 ,您可以从中修改响应请求。

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

您可以像这样实例化HttpClient:

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

我有类似的问题,但不太一样。 就我而言,我也遇到了重定向问题,但安全性是通过OAuth实现的,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 -> Headers.AuthenticationHeaderValue

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

给定一个名为refreshAuth的刷新函数,您可以创建一个新的HttpClient对象,如下所示:

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

Chris O'Neill发布的答案注意检查新URL是否仍然被认为是安全的。 我在此省略了安全性考虑,但在重试请求之前,您应该强烈考虑包含类似的检查。