的浏览器中的跨域问题从源“xxx”访问“xxx”处的XMLHttpRequest已被CORS策略阻止:请求的资源上不存在“Access-Control-Allow-Origin”标头。什么是跨域?[1]跨域,这可能是前端面试中遇到最多的问题了,可能是因为跨域问题是浏览器环境特有的问题,随处可见,就像蚊子不仅盯着你肉身也无处不在你身边来转移你的心烦意乱。“你看,服务器端发起HTTP请求是不会出现跨域问题的。”说到跨域问题的解决方案,最流行也是最简单的就是CORS。CORSCORS代表跨源资源共享(CORS)。简而言之,就是在服务端的响应中加入几个header,让浏览器可以跨域访问资源。这个响应头的字段设置是Access-Control-Allow-Origin:*下面是最简单的CORS请求GET/HTTP/1.1Host:shanyue.techOrigin:http://shanyue.techUser-Agent:Mozilla/5.0(Macintosh;IntelMacOSX10_14_3)AppleWebKit/537.36(KHTML,likeGecko)Chrome/83.0.4103.116Safari/537.36HTTP/1.1200OKAccess-Control-Allow-Origin:*Content-Type:text/plain;charset=utf-8Content-Length:12Date:Wed,08Jul202017:03:44GMTConnection:keep-alive预请求和Options当请求是跨域的,不是简单的请求时,会发起一个预请求,也就是Options。如果没有前置请求,万一直接执行破坏性的POST跨域请求,虽然最后浏览器告知你没有跨域权限,但是损失已经造成了,所以不是很大损失。以下条件构成一个简单的请求:Method:请求方式为GET、POST、HEADHeader:请求头为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:请求允许的方法,“在preflightrequests中使用”Access-Control-Allow-Headers:请求允许的headers,“在preflight中使用”request(preflightrequest)》Access-Control-Max-Age:预请求缓存时间responseheaders?关于cors的响应头有哪些?[2]?「关于CORS的设置就是CORS相关响应头的设置,所以了解这些头非常重要。不管配置的生产者和消费者,后端和前端,你都应该掌握!”下面是CORS相关的响应头及其解释Access-Control-Allow-Origin:资源可以共享那些域名,支持*和特定域名Access-Control-Allow-Credentials:请求是否可以携带cookiesAccess-Control-Allow-Methods:请求允许的方法,"usedinpreflightrequest"Access-Control-Allow-Headers:请求允许的headers,“usedinpreflightrequest”Access-Control-Expose-Headers:这些headers可以在响应中列出Access-Control-Max-Age:预请求缓存时间和关于CORS的中间件是为了使用默认值和配置来设置这些headers,比如koa/cors需要传递如下参数。/***CORSmiddleware**@param{Object}[options]*-{String|Function(ctx)}origin`Access-Control-Allow-Origin`,defaultisrequestOriginheader*-{String|Array}allowMethods`Access-Control-Allow-Methods`,默认为'GET,HEAD,PUT,POST,DELETE,PATCH'*-{String|Array}exposeHeaders`Access-Control-Expose-Headers`*-{String|Array}allowHeaders`Access-Control-允许-Headers`*-{String|Number}maxAge`Access-Control-Max-Age`inseconds*-{Boolean|Function(ctx)}credentials`Access-Control-Allow-Credentials`,默认为false.*-{boolean}keepHeadersOnErrorAddsetheadersto`err.header`ifanerroristrown*@return{Function}corsmiddleware*@apipublic*///Exampleapp.use(cors())CORS如何设置多个域名从上面看,好像很简单,只需要设置Access-Control-Allow-Origin可以轻松解决问题,但其中的陷阱可能比你想象的要多得多!先说Access-Control-Allow-Origin,它只允许两个值*:所有域名shanyue.tech:特定域名此时,新问题来了:?CORS需要指定多个域怎么办names[3]?“如果使用Access-Control-Allow-Origin:*,则所有请求都不能携带cookies”,所以这个方案被废弃了。所以这个问题需要通过写代码来解决。根据请求头中的Origin设置响应头Access-Control-Allow-Origin。如果请求头不包含Origin,则证明没有跨域,不做任何处理。如果请求头中包含Origin,为了证明跨域,根据Origin设置对应的Access-Control-Allow-Origin://获取Origin请求头constrequestOrigin=ctx.get('Origin');//如果not,skipif(!requestOrigin){returnawaitnext();}//设置响应头ctx.set('Access-Control-Allow-Origin',requestOrigin)"但是这时候会出现新的问题:caching》CORS与Vary:Origin在讨论与Vary的关系?如何防止CDN为PC端缓存移动端页面[4]?假设有两个域名访问跨域资源foo.shanyue.techstatic.shanyue.tech,响应头返回Access-Control-Allow-Origin:foo.shanyue.techbar.shanyue.tech,Access-Control-Allow-Origin:bar.shanyue.tech在响应头返回。一切看似正常,平静的水面却波涛汹涌:“如果static.shanyue.tech资源被CDN缓存了,当bar.shanyue.tech再次访问该资源时,由于缓存问题,返回的是Access-Control-Allow-Origin:foo.shanyue.tech,此时会出现跨域的问题”这时候Vary:Origin就发挥作用了,也就是针对不同的origin缓存不同的资源,也可以体现在各个server端的CORS中间件,比如下面几段代码,这里是一个koa关于CORS的处理函数:详见koajs/cors[5]returnasyncfunctioncors(ctx,next){//IftheOriginheaderisnotpresenterminatethissetofsteps.//Therequestisoutsidethescopeofthisspecification.constrequestOrigin=ctx.get('Origin');//AlwayssetVaryheader//https://github.com/rs/cors/issues/10ctx.vary('Origin');}这里是一段Go语言的CORS处理函数:参见rs/cors[6]func(c*Cors)handleActualRequest(whttp.ResponseWriter,r*http.Request){headers:=w.Header()origin:=r.Header。Get("Origin")//AlwayssetVary,参见https://github.com/rs/cors/issues/10headers.Add("Vary","Origin")}进一步完善相关代码://GetOriginrequestheaderconstrequestOrigin=ctx.get('Origin');//无论是否跨域都必须设置Vary:Originctx.set('Vary','Origin')//不设置则表示不跨域-domain,skipif(!requestOrigin){returnawaitnext();}//设置响应头ctx.set('Access-Control-Allow-Origin',requestOrigin)"那这时候CORS的问题不就解决了吗?从中间件处理层面来说是这样,但是还存在一些服务端中间件使用问题和浏览器问题。”HSTS和CORSHSTS(HTTPStrictTransportSecurity)为了避免HTTP跳转到HTTPS时潜在的中间人攻击,浏览器自己控制跳转到HTTPS和CORS一样,它也有一个服务器响应头来控制Strict-Transport-Security:max-age=5184000,此时浏览器访问域名时,会使用307InternalRedirect,无需服务器干预,自动跳转到HTTPS请求。“如果前端访问的是HTTP跨域请求,此时浏览器通过HSTS跳转到HTTPS,但浏览器不会给出相应的CORS响应头,就会出现跨域问题。”GET/HTTP/1.1Host:shanyue.techOrigin:http://shanyue.techUser-Agent:Mozilla/5.0(Macintosh;IntelMacOSX10_14_3)AppleWebKit/537.36(KHTML,likeGecko)Chrome/83.0.4103.116Safari/537.36AccesstoXMLHttpReginquestat'xxx'fromorique'xxx'hasbeenblockedbyCORSpolicy:-Control-Allow-Origin'标头存在于请求的资源上。服务器异常处理和跨域异常在与其他中间件配合使用时,也可能会出现问题,也可能会因为执行顺序不正确而导致跨域失败。假设有一个参数校验中间件,放在CORS中间件之上。因为验证不通过,没有通过CORS中间件,前端会报错。跨域失败,掩盖了真正的参数验证问题。constKoa=require('koa')constapp=newKoa()constcors=require('@koa/cors')//异常处理中间件app.use(async(ctx,next)=>{try{awaitnext()}catch(e){ctx.body='hello,error'}})//中间件app.use(async(ctx,next)=>{thrownewError('hello,world')}肯定会在某个时刻报错)//CORS中间件app.use(cors())app.listen(3000)总结本文介绍了跨域问题和对应的CORS解决方案,并列举了几个详细的问题。CORS通过在服务器端设置几个响应头来正常工作。Access-Control-Allow-Origin:*不能携带cookies,所以这是一个有缺陷的多域名跨域设置。服务器端通过响应头Origin来判断是否是跨域请求,并且这种方式跨域设置了多个域名,但是Vary:Origin要注意HSTS配置和服务器带来的潜在风险编码过程中的中间件顺序。参考[1]什么是跨域?:https://q.shanyue.tech/fe/js/216.html[2]cors的响应头有哪些:https://q.shanyue.tech/base/http/328.html[3]CORS如果需要指定多个域名怎么办:https://q.shanyue.tech/base/http/364.html[4]如何避免PC端CDN缓存手机页面:https://q.shanyue.tech/base/http/330。html[5]koajs/cors:https://github.com/koajs/cors/blob/master/index.js#L54[6]rs/cors:https://github.com/rs/cors/blob/be1c7e127af9fce006600894df5c5731d99cdc82/cors.go#L268本文转载自微信公众号“全栈成长之路”,可通过以下二维码关注。转载本文请联系全栈成长之路公众号。
