asp.net core中负载均衡场景下http重定向https的问题

上周欣喜地发现,微软官方终于针对 asp.net core 在使用负载均衡的情况下从 http 强制重定向至 https 的问题提供了解决方法。

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedProto
});

var options = new RewriteOptions()
    .AddRedirectToHttpsPermanent();
app.UseRewriter(options);

但实际使用之后,欣喜变成了失望 —— 微软对这个问题的认识角度和我们不一样,造成这个方法对我们不适用,不得不继续使用我们的土方法。

为什么会这样?请看下面的分解。 

AddRedirectToHttpsPermanent 早就在 BasicMiddleware 的 RedirectToHttpsRule 中实现了,它的逻辑很简单 —— 判断当前请求是否是https,如果不是就进行重定向。

if (!context.HttpContext.Request.IsHttps)
{
    //...
}

这个直接了当的判断在使用负载均衡的场景下不仅不会发挥应有的作用,而且会产生致命的副作用 —— 让请求进入重定向死循环(ERR_TOO_MANY_REDIRECTS)。因为不管客户端的请求是 http 还是 https ,负载均衡与后端服务器之间始终是 http(当然你可以用https,但那是吃饱了撑着还浪费粮食)。如果负载均衡不额外提供这个信息,在后端服务器的眼里始终只有 http 没有 https ,http 重定向 https 根本无法实现。

从负载均衡的角度,为了解决这个问题,通常会通过一个另外的专用的请求头抓发这个信息,它的名字叫"X-Forwarded-Proto"。

从 asp.net core 的角度,要解决这个问题,需要弥补 Request.IsHttps 与 X-Forwarded-Proto 之间的鸿沟。于是微软实现了上面的 app.UseForwardedHeaders() ,实际是由 ForwardedHeadersMiddleware 完成这个任务 —— 根据 X-Forwarded-Proto 设置 Scheme(Request.IsHttps 就是基于 Scheme 进行判断的)。

if (checkProto && i < forwardedProto.Length)
{
    set.Scheme = forwardedProto[forwardedProto.Length - i - 1];
}

到此为止,微软完美地解决了这个问题,RedirectToHttpsRule 不用修改1行代码。

但是在实际使用时,我们发现一个大问题,大到我们必须弃用这个看似完美的解决方法。

微软解决 http to https 问题的思路是这样:只要请求不是 https 的,就强制跳转到 https(这个没问题),其他一概不管,不管这个请求是不是来自负载均衡转发的(这个不够贴心)。

而我们要解决的问题是:只有在负载均衡转发的原始请求是 http 的情况下,才强制跳转至 https 。比如在服务器本机访问,比如来自其他docker容器的访问,如果这也跳转,那每台服务器(或者docker容器)都要部署https证书,多麻烦。

一个是只要不是 https ,就跳转;一个是只有是转发的 http ,才跳转。 就是因为这个对问题理解的差异,我们不得不放弃采用微软的官方解决方法,继续使用我们不太优雅的土方法。

RedirectToProxiedHttpsRule

public class RedirectToProxiedHttpsRule : RedirectToHttpsRule
{
    public RedirectToProxiedHttpsRule()
    {
        base.StatusCode = StatusCodes.Status301MovedPermanently;
        base.SSLPort = null;
    }

    public override void ApplyRule(RewriteContext context)
    {
        var key = "X-Forwarded-Proto";
        var request = context.HttpContext.Request;
        if (request.Headers.ContainsKey(key))
        {
            if (request.Headers[key].FirstOrDefault() == "http")
            {
                base.ApplyRule(context);
            }
        }
    }
}

RewriteOptionsExtensions

public static class RewriteOptionsExtensions
{
    public static RewriteOptions AddRedirectForwardedHttpToHttps(this RewriteOptions options)
    {
        options.Rules.Add(new RedirectToProxiedHttpsRule());
        return options;
    }
}

在 Startup 中使用

var options = new RewriteOptions()
    .AddRedirectForwardedHttpToHttps();
app.UseRewriter(options);
posted @ 2017-06-18 19:58 dudu 阅读(...) 评论(...) 编辑 收藏