静态化资源在混合协议下的跨域解决方案

猪小花1号2018-08-31 12:28

作者:牛洋

1 问题背景

    618大促中,运营首次使用落地页的优惠券模块配置神券活动,活动效果非常明显,流量瞬间增加130%。但在流量的冲击下,领券慢请求对nginx连接大量占用,导致主站部分核心业务不可用。为了避免落地页服务对其他核心业务的影响,活动主对落地页相关请求的域名进行了改造(独立了nginx),将原来的m.kaola.com域独立为huodong.kaola.com域。
    huodong.kaola.com域的改造并不顺利,主要是跨域导致的请求失败。解决问题的初期,受到经验主义的影响,简单地认为对落地页相关请求配置cors拦截器即可解决跨域问题,但上线后发现跨域问题仍然存在。经过深入的讨论分析,发现落地页面临的跨域场景和常规场景有差异,差异在于:
    (1)http和https混合协议的使用
    (2)对静态化资源的跨域访问(使用varnish和nginx-cache做了缓存)

2 基于CORS的常规跨域处理方法
    在给出跨域解决方案之前,先对常规的跨域处理方法进行说明,并分析常规方案在落地页场景下的缺陷。所谓的跨域问题常规解决方案,即通过filter或拦截器在response中设置cors相关http header,从而允许部分请求跨域,在wap的实现中会允许域名包含kaola.com的所有origin跨域。 下面从请求非静态化资源和静态化资源两个场景对常规的跨域解决方案进行分析。
2.1 请求非静态化资源
    
    非静态化资源的请求处理可以简化为上图所示的流程(图中的场景是在http://m.kaola.com域下发出复杂请求http://huodong.kaola.com/activity/h5/1234.html),下面对请求过程进行简单的说明。
    (1)由于请求是复杂请求,在正式请求发出之前会发出OPTIONS请求进行预检,预检请求的返回结果会包含如图所示的CORS相关http首部,OPTIONS请求的结果会根据返回首部的Max-Age进行缓存。
    (2)请求通过预检后,发出正式请求,http首部Allow-Origin告诉浏览器允许在http://m.kaola.com域跨域请求资源http://huodong.kaola.com/*,则正式请求可以正常进行。
    由于请求每次都会到达tomcat,因此,只需要根据request的Origin动态设置Allow-Origin即可解决在没有静态化资源情况下的跨域问题。
2.2 混合协议下请求静态化资源
    在常规方案下,若跨域请求静态化资源,则部分的请求流程如上图所示(图中的场景是在http://m.kaola.com和https://m.kaola.com域下发出请求http://huodong.kaola.com/activity/h5/1234.shtml),对上图进行简单的说明:
    (1)若服务端除了tomcat层,还使用了varnish或nginx-cache,当请求没有命中cache时,会接着发往tomcat,tomcat对请求进行处理,并通过拦截器设置cors首部
    (2)tomcat层返回的结果会被缓存到cache中,此时,若初次发出请求的页面是http://m.kaola.com, 缓存时间是1min, 则Allow-Origin=http://m.kaola.com会被缓存1min
    (3)1min内,http://m.kaola.com域再次发出请求,则会直接从cache获取返回结果
    (4)但1min内,若有客户端从https://m.kaola.com域发出请求,由于https://m.kaola.com和Allow-Origin的http://m.kaola.com不匹配,则会出现跨域问题。
    综上所述,常规方案在跨域访问静态化资源且在混合协议下时存在问题。
3 静态化资源在混合协议下的跨域解决方案
    从2中可知,静态化资源在混合协议下的跨域主要源于varnish或nginx-cache等会缓存http response的全部内容,因此在缓存有效期内,浏览器接收的response的内容固定。若用常规方式--通过拦截器设置cors相关的http header,则在客户端协议不固定的情况下,就可能出现跨域问题。
    为了解决上述问题,活动组的思路是去掉拦截器对cors头的设置,只从请求出口处设置或替换http header, 以避免跨域相关请求头被缓存。由于目前活动组前台业务还在wap中开发,因此面临对业务层跨域逻辑的改造和nginx跨域配置的改造,下面给出对nginx和对tomcat拦截器的具体改造方案。
3.1 nginx配置
    以下是通过nginx判断跨域并设置cors首部的配置。
set $origin_host "";
set $is_cors "false";
set $cors_type "others";

if ($http_origin ~* ^https?://([a-zA-Z0-9\.]+)) {
    set $origin_host $1;
}

if ($origin_host ~* ^[\w-]+\.kaola\.com(\.\w+)?(:\d+)?$) {
    set $is_cors "true";
}

if ($origin_host = $host) {
    set $is_cors "false";
}

if ($request_method = "OPTIONS") {
    set $cors_type "options";
}

if ($is_cors = "false") {
    set $cors_type "";
}

if ($is_cors = "true") {
    add_header "Access-Control-Allow-Origin" "$http_origin";
    add_header "Access-Control-Allow-Methods" "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH";
    add_header "Access-Control-Allow-Headers" "Content-Type,X-Requested-With,ursAuth,origin,ursid, urstoken,x-test";
    add_header "Access-Control-Allow-Credentials" "true";
    add_header "Access-Control-Max-Age" "86400";
}

if ($cors_type = "options") {
    return 204;
}
3.2 CORS拦截器改造
    由于nginx的 add_header操作执行的是append操作,并不会对tomcat层传回的header进行覆盖,因此,需要在拦截器中去除对由nginx设置cors头部的域名的处理( isProcessCorsByNginx方法 ), isProcessCorsByNginx方法可以采用硬编码、disconf或者由nginx层透传一个标记(推荐)实现。下面给出拦截器的核心代码,和硬编码方式实现的isProcessCorsByNginx,其他实现方式类似,不赘述。
try {
    // 从header获取origin
    String origin = request.getHeader("Origin");
    if (checkOrigin(origin) && !isProcessCorsByNginx(request)) {
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE,OPTIONS");
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With,ursAuth,origin, ursid, urstoken");
        response.setHeader("Access-Control-Allow-Credentials", "true");
    }
    // 如果是options请求则直接返回204
    if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
        response.setStatus(HttpStatus.NO_CONTENT.value());
        return false;
    }
} catch (Exception e) {
    LogConstant.runLog.info("设置CROS信息异常", e);
}
return true;
private boolean isProcessCorsByNginx(HttpServletRequest request) throws MalformedURLException {
        if (request == null || request.getRequestURL() == null) {
            return false;
        }
        String requestUrl = request.getRequestURL().toString();
        URL url = new URL(requestUrl);
        String host = url.getHost();
        return "huodong.kaola.com".equals(host);
    }
3.3 上线流程
(1)增加开关控制,增加huodong域和考拉域的开关,nginx配置上线之前需要关闭开关,所有落地页相关请求需走m.kaola域
(2)按3.2所示修改CORS拦截器并上线
(3)增加nginx配置并上线
(4)打开开关

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者牛洋授权发布。