实现跨域ajax请求的方式有不少,其中一个是利用CORS,而这个方法关键是在服务器端进行配置。ajax
本文仅对可以完成正常跨域ajax响应的,最基本的配置进行说明(深层次的配置我也不会)。json
CORS将请求分为简单请求和非简单请求,能够简单的认为,简单请求就是没有加上额外请求头部的get和post请求,而且若是是post请求,请求格式不能是application/json(由于我对这一块理解不深若是错误但愿能有人指出错误并提出修改意见)。而其他的,put、post请求,Content-Type为application/json的请求,以及带有自定义的请求头部的请求,就为非简单请求。跨域
简单请求的配置十分简单,若是只是完成响应就达到目的的话,仅需配置响应头部的Access-Control-Allow-Origin便可。数组
若是咱们在http://localhost:3000 域名下想要访问 http://127.0.0.1:3001 域名。能够作以下配置:服务器
app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000'); await next(); });
而后用ajax发起一个简单请求,例如post请求,就能够轻松的获得服务器正确响应了。
实验代码以下:cookie
$.ajax({ type: 'post', url: 'http://127.0.0.1:3001/async-post' }).done(data => { console.log(data); })
服务器端代码:app
router.post('/async-post',async ctx => { ctx.body = { code: "1", msg: "succ" } });
而后就能获得正确的响应信息了。
这时候若是看一下请求和响应的头部信息,会发现请求头部多了个origin(还有一个referer为发出请求的url地址),而响应头部多了个Access-Control-Allow-Origin。async
如今能够发送简单请求了,可是要想发送非简单请求仍是须要其余的配置。post
当第一次发出非简单请求的时候,实际上会发出两个请求,第一次发出的是preflight request,这个请求的请求方法是OPTIONS,这个请求是否经过决定了这一个种类的非简单请求是否能成功获得响应。性能
为了能在服务器匹配到这个OPTIONS类型的请求,所以须要本身作一个中间件来进行匹配,并给出响应使得这个预检可以经过。
app.use(async (ctx, next) => { if (ctx.method === 'OPTIONS') { ctx.body = ''; } await next(); });
这样OPTIONS请求就可以经过了。
若是检查一下preflight request的请求头部,会发现多了两个请求头。
Access-Control-Request-Method: PUT Origin: http://localhost:3000
要经过这两个头部信息与服务器进行协商,看是否符合服务器应答条件。
很容易理解,既然请求头多了两个信息,响应头天然也应该有两个信息相对应,这两个信息以下:
Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: PUT,DELETE,POST,GET
第一条信息和origin相同所以经过。第二条信息对应Access-Controll-Request-Method,若是在请求的方式包含在服务器容许的响应方式之中,所以这条也经过。两个约束条件都知足了,因此能够成功的发起请求。
至此为止,至关于仅仅完成了预检,还没发送真正的请求呢。
真正的请求固然也成功得到了响应,而且响应头以下(省略不重要部分)
Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: PUT,DELETE,POST,GET
请求头以下:
Origin: http://localhost:3000
这就很显而易见了,响应头部信息是咱们在服务器设定的,所以是这样。
而客户端由于刚才已经预检过了,因此不须要再发Access-Control-Request-Method这个请求头了。
这个例子的代码以下:
$.ajax({ type: 'put', url: 'http://127.0.0.1:3001/put' }).done(data => { console.log(data); });
服务器代码:
app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000'); ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET'); await next(); });
至此咱们完成了可以正确进行跨域ajax响应的基本配置,还有一些能够进一步配置的东西。
好比,到目前为止,每一次非简单请求都会实际上发出两次请求,一次预检一次真正请求,这就比较损失性能了。为了能不发预检请求,能够对以下响应头进行配置。
Access-Control-Max-Age: 86400
这个响应头的意义在于,设置一个相对时间,在该非简单请求在服务器端经过检验的那一刻起,当流逝的时间的毫秒数不足Access-Control-Max-Age时,就不须要再进行预检,能够直接发送一次请求。
固然,简单请求时没有预检的,所以这条代码对简单请求没有意义。
目前代码以下:
app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000'); ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET'); ctx.set('Access-Control-Max-Age', 3600 * 24); await next(); });
到如今为止,能够对跨域ajax请求进行响应了,可是该域下的cookie不会被携带在请求头中。若是想要带着cookie到服务器,而且容许服务器对cookie进一步设置,还须要进行进一步的配置。
为了便于后续的检测,咱们预先在http://127.0.0.1:3001这个域名下设置两个cookie。注意不要错误把cookie设置成中文(刚才我就设置成了中文,结果报错,半天没找到出错缘由)
而后咱们要作两步,第一步设置响应头Access-Control-Allow-Credentials为true,而后在客户端设置xhr对象的withCredentials属性为true。
客户端代码以下:
$.ajax({ type: 'put', url: 'http://127.0.0.1:3001/put', data: { name: '黄天浩', age: 20 }, xhrFields: { withCredentials: true } }).done(data => { console.log(data); });
服务端以下:
app.use(async (ctx, next) => { ctx.set('Access-Control-Allow-Origin', 'http://localhost:3000'); ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET'); ctx.set('Access-Control-Allow-Credentials', true); await next(); });
这时就能够带着cookie到服务器了,而且服务器也能够对cookie进行改动。可是cookie还是http://127.0.0.1:3001域名下的cookie,不管怎么操做都在该域名下,没法访问其余域名下的cookie。
如今为止CORS的基本功能已经都提到过了。
一开始我不知道怎么给Access-Control-Allow-Origin,后来经人提醒,发现能够写一个白名单数组,而后每次接到请求时判断origin是否在白名单数组中,而后动态的设置Access-Control-Allow-Origin,代码以下:
app.use(async (ctx, next) => { if (ctx.request.header.origin !== ctx.origin && whiteList.includes(ctx.request.header.origin)) { ctx.set('Access-Control-Allow-Origin', ctx.request.header.origin); ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET'); ctx.set('Access-Control-Allow-Credentials', true); ctx.set('Access-Control-Max-Age', 3600 * 24); } await next(); });
这样就能够不用*通配符也可匹配多个origin了。
注意:ctx.origin与ctx.request.header.origin不一样,ctx.origin是本服务器的域名,ctx.request.header.origin是发送请求的请求头部的origin,两者不要混淆。
最后,咱们再稍微调整一下自定义的中间件的结构,防止每次请求都返回Access-Control-Allow-Methods以及Access-Control-Max-Age,这两个响应头实际上是没有必要每次都返回的,只是第一次有预检的时候返回就能够了。
调整后顺序以下:
app.use(async (ctx, next) => { if (ctx.request.header.origin !== ctx.origin && whiteList.includes(ctx.request.header.origin)) { ctx.set('Access-Control-Allow-Origin', ctx.request.header.origin); ctx.set('Access-Control-Allow-Credentials', true); } await next(); }); app.use(async (ctx, next) => { if (ctx.method === 'OPTIONS') { ctx.set('Access-Control-Allow-Methods', 'PUT,DELETE,POST,GET'); ctx.set('Access-Control-Max-Age', 3600 * 24); ctx.body = ''; } await next(); });
这样就减小了多余的响应头。