本文仅供自己学习使用
什么是跨域
协议,域名,端口,三者有一个不一样,就是跨域。
如何解决跨域
- CORS(Cross-Orign Resource Sharing),在服务器端设置几个响应头
- Reverse Proxy,在nginx/traefik/haproxy等反向代理服务器中设置为同一域名。
CORS
CORS即跨域资源共享(Cross-Origin Resource Sharing,CORS)。简而言之,就是在服务器端的响应中加入几个标头,使得浏览器能够跨域访问资源。
这个响应头的字段设置就是 Access-Control-Allow-Origin: *,如下图所示
以下是一个最简单的CORS请求
GET / HTTP /1.1
Host: shanyue.tech
Origin: http://shanyue.tech
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Date: Wed, 02 Sep 2020 11:03:44 GMT
Connection: keep-alive
预请求与Options 当一个请求跨域且不是简单请求时就会发起预请求,也就是Options。如果没有预请求,万一有一个毁灭性的POST跨域请求直接执行,虽然最后告知浏览器你没有跨域权限,但是损失已造成.
以下条件构成了简单请求:
- Method:请求的方法是GET、POST及HEAD
- Header:请求头是Content-Type(有限制)、Accept-Language、Content-Language 等
- Content-Type:请求类型是application/x-www-form-urlencoded、multipart/form-data或text/plain
非简单请求一般需要开发者主动构造,在项目常见的 Content-Type:application/json 及 Authorization:为典型的非简单请求。与之相关的三个字段如下:
- Access-Control-Allow-Methods:请求所允许的方法,用于预请求(preflight request)中
- Access-Control-Allow-Headers:请求所允许的头,用于预请求(preflight request)中
- Access-Control-Max-Age:预请求的缓存时间
CORS Middleware
关于CORS的设置即是对CORS相关响应头的设置。
CORS的请求头有:
- Access-Control-Allow-Origin:可以把资源共享给哪些域名,支持*及特定域名
- Access-Control-Allow-Methods:请求是否可以带cookie
- Access-Control-Allow-Headers:请求所允许的方法,用于预请求(preflight request)中
- Access-Control-Allow-Credentials(请求所允许的头,用于预请求(preflight request)中
- Access-Control-Expose-Headers:哪些头可以在响应中列出
- Access-Control-Max-Age:预请求的缓存时间
而关于CORS的中间件既是使用默认值与配置来设置这些头,如koa/cors需要传递以下参数。
/**
* CORS middleware
*
* @param {Object} [options]
* - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
* - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
* - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
* - {String|Array} allowHeaders `Access-Control-Allow-Headers`
* - {String|Number} maxAge `Access-Control-Max-Age` in seconds
* - {Boolean|Function(ctx)} credentials `Access-Control-Allow-Credentials`, default is false.
* - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
* @return {Function} cors middleware
* @api public
*/
// Example
app.use(cors())
CORS如何设置多域名
Access-Control-Allow-Origin,它所允许的值只有两个
- *:所有域名
- xianyu.sky:特定域名
那么CORS如果需要指定多个域名怎么办?
如果使用 Access-Control-Allow-Origin:*,则所有的请求不能够携带cookie,因此这种方案被摈弃。
因此这个问题需要写代码来解决,根据请求头中的Origin来设置响应头。
请求头:Origin
并不是所有请求都会自动带上Origin,在浏览器中带 Origin 的逻辑如下
- 如果存在跨域,则带上Origin,值为当前域名。
- 如果不存在跨域,则不带Origin 逻辑理清楚后,关于服务器中对于 Access-Control-Allow-Origin 设置多域名的逻辑也很清晰了。
- 如果请求头不带有Origin,证明未跨域,则不作任何处理
- 如果请求头带有Origin,证明跨域,根据Origin设置相应的Access-Control-Allow-Origin:
使用伪代码实现如下:
// 获取Origin请求头
const requestOrigin = ctx.get('Origin');
// 如果没有,则跳过
if(!requestOrigin){
return await next();
}
// 设置响应头
ctx.set('Access-Control-Allow-Origin',requestOrigin)
CORS与Vary:Origin
如何避免CDN为PC端缓存移动端页面
如果PC端和移动端是一套代码则不会出现这个问题,这个问题出现在PC端和移动端是两套代码,但是却共用了一个域名。
使用 nginx 配置如下,根据UA判断是否移动端,而走不同的逻辑(判断UA是否移动端容易出问题)
location / {
// 默认 PC 端
root /usr/local/website/web;
# 判断UA,访问移动端
if ($http_user_agent ~* "(Android|webOS|iPhone|iPad|BlcakBerry)")
root /usr/local/websitemobile;
}
index index.html index.htm;
解决方案通常使用 Vary 响应头,来控制CDN对不同请求头的缓存。
此处可以使用 Vary: User-Agent,代表如果User-Agent不一样,则重新发起请求,而非从缓存中读取页面
Vary: User-Agent
当然,User-Agent实在过多,此时缓存失效就会过多。
简答
使用Vary:User-Agent,根据UA进行缓存。
Vary:User-Agent
最好不要出现这种情况,PC端和移动端如果是两代码,建议用两个域名,理由如下
- nginx 判断是否移动端容易出错
- 对缓存不友好
假设有两个域名可以访问 static.xianyu.sky 的跨域资源
- foo.xianyu.sky,响应头中返回 Access-Control-Allow-Origin:foo.xianyu.sky
- bar.xianyu.sky,响应头中返回 Access-Control-Allow-Origin:bar.xianyu.sky
如果static.xianyu.sky 资源被CDN缓存,bar.xianyu.sky 再次访问资源时,因缓存问题,因此此时返回的是 Access-Control-Allow-Origin:foo.xianyu.sky,此时会有跨域问题。
此时,Vary:Origin就上场了,代表为不同的Origin 缓存不同的资源,这在各个服务器端CORS中间也能体现出来。
如下是koa关于CORS的处理函数
return async function cors(ctx, next) {
// If the Origin header is not present terminate this set of steps.
// The request is outside the scope of this specification.}
const requestOrigin = ctx.get('Origin');
// Always set Vary header
// https: //github.com/rs/cors/issues/10
ctx.vary('Origin');
如下是Go语言关于CORS的处理函数
func (c*Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin :=r.Header.Get("Origin")
// Always set Vary,see https://github.com/rs/cors/issues/10
headers.Add("Vary","Origin")
}
进一步改进相关代码:
// 获取Origin请求头
const requestOrigin = ctx.get('Origin');
// 不管有没有跨域都要设置 Vary: Origin
ctx.set('Vary', 'Origin')
// 如果没有设置,说明没有跨域,跳过
if (!requestOrigin)
{
return await next();
}
// 设置响应头
ctx.set('Access-Control-Allow-Origin',requestOrigin)
HSTS与CORS
HSTS(HTTP Strict Transport Security)为了避免HTTP跳转到HTTPS时遭受潜在的中间人攻击,由浏览器本身控制到HTTPS的跳转。如同CORS一样,它也是有一个服务器的响应头来控制。
Strict-Transport-Security:max-age=5184000
此时浏览器访问该域名时,会使用307 Internal Redirect,无需服务器干涉,自动跳转到HTTPS请求。 如果前端访问HTTP跨域请求,此时浏览器通过HSTS跳转到HTTPS,但浏览器不会给出相应的CORS响应头部,就会发生跨域问题。
GET / HTTP/1.1
Host: xianyu.sky
Origin: http://xianyu.sky
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Edg/85.0.564.44
Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
服务器异常处理与跨域异常
当与其他中间件一起工作时,也有可能出现问题,由于不正确的执行顺序也可能导致跨域失败。
假设有一个参数校验中间件,置于CORS中间件上方,由于校验失败,并未穿过CORS中间件,在前端会报错跨域失败,真正的参数校验问题掩盖其中。
const Koa = require('koa')
const app = new Koa()
const cors = require('@koa/cors')
// 异常处理中间件
app.use(async(ctx, next) => {
try {
await next()} catch (e) {
ctx.body = 'hello,error'}
})
// 某一个特定时刻肯定会报错的中间件
app.use(async(ctx, next) => {
throw new Error('hello,world')})
// 某一个特定时刻肯定会报错的中间件
app.use(async (ctx,next) => {
throw new Error('hello,world')})
// CORS中间件
app.use(cors())
app.listen(3000)
总结
本篇文章介绍了跨域问题及其相应的CORS解决方案,并列出若干细节问题。
- CORS通过服务器端设置若干响应头来正常工作
- Access-Control-Allow-Origin:* 无法携带Cookie,因此以此为多域名跨域设置有缺陷
- 服务器端通过响应头Origin来判断是否为跨域请求,并以此设置多域名跨域,但要加上Vary:Origin
- 在编码过程中要注意HSTS配置及服务器的中间件顺序带来的潜在风险