随着公司业务慢慢变复杂, 会分拆业务模块到不同的团队, Dashboard 会变成一个大杂烩, 各个团队都往上加功能.

假设 Dashboard 是一个纯前端项目, 这里只讨论 CORS( Cross-Origin Resource Sharing ) 这个点.

https://mdn.mozillademos.org/files/14295/CORS_principle.png

如上图, 假设 Dashboard 架设在 www.icbd.devel/dashboard, 它请求本网站的资源都没有问题:

访问本站资源

如果 Dashboard 架设在 localhost:63342, 它请求 www.icbd.devel 的资源就会多出一个 OPTIONS 请求:

跨站请求

CORS 的限制起源于浏览器的同源策略, 浏览器默认只允许本站的脚本访问本站的资源, 如果要发起跨站请求的话, 需要向相应的站点询问, 即发起 OPTIONS 请求.

OPTIONS 请求头中会携带这两个关键信息:

Origin: http://localhost:63342
Access-Control-Request-Method: POST

如果服务器接受你的请求, OPTIONS 会响应 200, 响应头会包含:

Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS

这里标注了: 该来源, 访问该resource, 所允许的所有 HTTP Method .

否则, 标明服务器拒绝该请求, 浏览器会自动判断从而block你的跨站请求.

Rack::Cors

对于应该如何响应 OPTIONS 请求, Rack::Cors 可以帮我们方便地配置.

class App < Sinatra::Application
  use Rack::Cors do |cors_self|
    cors_self.allow do |allow_resources|
      allow_resources.origins /http:\/\/localhost\S*/
      allow_resources.resource '*', headers: :any, methods: [:get, :post, :put, :options]
    end
  end
  # ...
end 

配置 origins 需要注意的点是:

‘*’ 表示通配所有; 其他情况下*就是普通字符, 应使用正则表达式本身的语法:

cors.rb line:274

    def origins(*args, &blk)
      @origins = args.flatten.reject{ |s| s == '' }.map do |n|
        case n
        when Proc,
             Regexp,
             /^https?:\/\//,
             'file://'        then n
        when '*'              then @public_resources = true; n
        else                  Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
        end
      end.flatten
      @origins.push(blk) if blk
    end

Rack::Cors 的核心原理就是拦截 OPTIONS 的请求, 如果能匹配到合适的 allow 规则, 就直接 return [200, headers, []], 这意味着不会执行到 @app.call env, 也就是截断了中间件调用链.

如果匹配不到 allow 规则, 然后调用 @app.call env, 把流量交给后续的中间件(Sinatra 或 Rails 就是位置偏后的中间件), 待其他中间件处理完之后, 最后把 add_headers 合并到总的 headers 上.

简化过的 Rack::Cors#call

    def call(env)
      env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]

      add_headers = nil
      if env[HTTP_ORIGIN]
        if env[REQUEST_METHOD] == OPTIONS and env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
          headers = process_preflight(env)
          return [200, headers, []]
        else
          add_headers = process_cors(env)
        end
      else
        Result.miss(env, Result::MISS_NO_ORIGIN)
      end

      status, headers, body = @app.call env

      if add_headers
        headers = add_headers.merge(headers)
      end

      [status, headers, body]
    end

基于以上原理得知, 一定要把 Rack::Cors 放到 Rack 中间件的的最前端:

    config.middleware.insert_before 0, Rack::Cors do
    # rules... 
    end

Conference

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://github.com/cyu/rack-cors