CORS and rack-cors
随着公司业务慢慢变复杂, 会分拆业务模块到不同的团队, Dashboard 会变成一个大杂烩, 各个团队都往上加功能.
假设 Dashboard 是一个纯前端项目, 这里只讨论 CORS( Cross-Origin Resource Sharing )
这个点.
如上图, 假设 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