.NET Core安全性增强导致HttpClientHandler的AllowAutoRedirect属性无效问题分析
最近在工作中发现一个莫名其妙的Bug,考察下面的代码:
var baseUrl = "https://test.example.com/"; var loginUrl = $"{baseUrl}sso-auth/login"; var userInfoUrl = $"{baseUrl}sso-auth/user-info"; var cookieAndCsrf = await GetNewSessionCookiesAndCSRFAsync(loginUrl).ConfigureAwait(false); var username = "username"; var password = "password"; var postFormContent = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("username", username), new KeyValuePair<string, string>("password", password), new KeyValuePair<string, string>("_csrf", cookieAndCsrf.Item2) }); var cookieContainer = new CookieContainer(); using var httpClientHandler = new HttpClientHandler() { CookieContainer = cookieContainer }; using var httpClient = new HttpClient(httpClientHandler); var request = new HttpRequestMessage(HttpMethod.Post, loginUrl) { Content = postFormContent }; request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); cookieAndCsrf.Item1.ToList().ForEach(k => cookieContainer.Add(new Uri(baseUrl), k)); var response = await httpClient.SendAsync(request).ConfigureAwait(false); response.EnsureSuccessStatusCode();这段代码的主要目的是为了从服务端的login的界面,通过以表单的方式传入用户名和密码,然后获取认证信息。在以前版本的产品中,这部分代码可以正常运行,然而,最近发现,当获取到response之后,HTTP的返回状态为302 Found,已经不再是200 OK了。 考虑到代码一直没有动过,唯一动过的地方就是升级了.NET,从原来的仅支持Windows的.NET Framework 4.7升级到了.NET 6,目的是为了能够让这个库跨平台使用。考虑到这一层面,我又新建了一个控制台应用程序,使其仅在.NET Framework 4.7下运行,然后重新调用上面的代码来观察response的状态码,发现确实为200 OK。至此,基本可以确定就是升级.NET版本所致。 经过一番查找和求证,发现.NET 6(其实从.NET Core 1.0开始)对于HttpClientHandler的AllowAutoRedirect属性的定义有一定的变化:当POST的response从https重定向到http时,在老版本的.NET中,如果设置AllowAutoRedirect为true(默认值是true),那么.NET Framework会自动帮你完成重定向,并得到最终一轮重定向的返回结果;而在.NET Core下,出于安全性考虑,如果是https重定向到http时,.NET将不再为你代劳,而直接返回3xx的代码,即使你的HttpClientHandler的AllowAutoRedirect被设置为true。微软官方文档也说明了这一点:
With AllowAutoRedirect set to(参考:https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.allowautoredirect?view=net-6.0#remarks) 回到上面的代码,可以看到,response.Location是一个http的地址,因此,在.NET 6中,返回的状态码就是302,而不是200: 在.NET Runtime的Github repo上,也有不少人提议,在HttpClientHandler上新加一个属性,用于启用从HTTPS到HTTP的重定向,或者提供另外的API以简化对于302返回代码的处理。然而,最终该帖子以“Won't Fix”的方式关闭了。参考:https://github.com/dotnet/runtime/issues/28039。 解决这个问题的方法就是自己写代码完成重定向。代码有很多种写法,下面就是一种比较简单的做法:true
, the .NET Framework will follow redirections even when being redirected to an HTTP URI from an HTTPS URI. .NET Core versions 1.0, 1.1 and 2.0 will not follow a redirection from HTTPS to HTTP even if AllowAutoRedirect is set totrue
.
private const int MaximumAutomaticRedirects = 50; private static readonly HttpStatusCode[] RedirectStatusCodes = new[] { HttpStatusCode.Moved, HttpStatusCode.MovedPermanently, HttpStatusCode.Found, HttpStatusCode.Redirect, HttpStatusCode.RedirectMethod, HttpStatusCode.SeeOther, HttpStatusCode.RedirectKeepVerb, HttpStatusCode.TemporaryRedirect }; private static async Task<HttpResponseMessage> SendWithRedirectAsync(HttpClient httpClient, HttpRequestMessage requestMessage) { var redirectCount = 0; var response = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); while (RedirectStatusCodes.Any(c => response.StatusCode == c)) { var nextRequestUri = response.Headers.Location; if (nextRequestUri == null) { throw new AuthenticationException("The response indicates a redirect, but the redirect URI is not specified in the Location header"); } if (++redirectCount == MaximumAutomaticRedirects) { throw new AuthenticationException($"Too many redirects. Maximum number of redirects is set to {MaximumAutomaticRedirects}."); } var nextRequest = new HttpRequestMessage(HttpMethod.Get, nextRequestUri); response = await httpClient.SendAsync(nextRequest).ConfigureAwait(false); } return response; }最后,只需要将第一段代码中的
var response = await httpClient.SendAsync(request).ConfigureAwait(false);改为如下即可:
var response = await SendWithRedirectAsync(httpClient, request).ConfigureAwait(false);