为您推荐:
精华内容
最热下载
问答
  • 点击上方前端瓶子君,关注公众号回复算法,加入前端编程面试算法每日一题群在 CORS 完全手册之如何解决CORS 问题?里面我们提到了常见的CORS 错误解法,以及大多数状况下应该要选择的...

    点击上方 前端瓶子君,关注公众号

    回复算法,加入前端编程面试算法每日一题群

    在 CORS 完全手册之如何解决CORS 问题?里面我们提到了常见的CORS 错误解法,以及大多数状况下应该要选择的解法:「请后端加上response header」。

    但其实「跨来源请求」这个东西又可以再细分成两种,简单请求跟非简单请求,简单请求的话可以透过上一篇的解法来解,但非简单请求的话就比较复杂一些了。

    除此之外,跨来源请求预设是不会把cookie 带上去的,需要在使用xhr 或是fetch 的时候多加一个设定,而后端也需要加一个额外的header 才行。

    与CORS 相关的header 其实不少,有些你可能听都没听过。原本这篇我想要把这些东西一一列出来讲解,但仔细想了一下觉得这样有点太无趣,而且大家应该看过就忘记了。

    那怎样的方法会比较好呢?大家都喜欢听故事,因此这篇让我们从故事的角度下手,为大家讲述一段爱与CORS 的故事。

    主角的名字大家都知道了,对,就是毫无新意的小明。

    Day1:简单的CORS

    小明任职于某科技公司,担任菜鸟前端工程师。

    而他的第一个任务,就是要做一个「联络我们」的表单,让看到官网,对他们服务有兴趣的潜在使用者能够联络到公司的人,再让业务去跟他们联络,洽谈后续的合作事项。

    而表单长这样(虽然长得很像Goolge 表单但是是小明自己做的):

    小明花了半天不到的时间,把页面都刻好了,功能也差不多做完了,只剩下最后一步而已。小明的主管跟他说公司常常会对外举办一些活动,而在活动尾声都会提供这个表单给大家,希望大家统一透过表单留下联络资料。

    因此表单上的「怎么知道我们公司的?」就会希望能够动态调整栏位,在活动期间加一个「透过在1/10 举办的技术分享会」的选项,而活动结束后大概两个礼拜把这个选项撤掉。之所以要能动态调整,主管说是因为不想让后续维护的工再回到开发这端,如果一开始就能做成动态的,那未来只要他们自己维护就行了,让他们能够透过后台自己去控制。

    所以后端开了一个API出来,要小明去接这个API然后把内容render出来变成选项。为了方便测试,后端工程师先把整个API service打包成docker image,然后让小明跑在自己电脑上,网址是:http://localhost:3000

    小明接到这个任务之后,想说先把API 内容抓下来看看好了,于是就写了这样一段程式码:

    fetch('http://localhost:3000')
    

    然后发现console 出现了错误讯息:

    小明没有看得很懂那是什么意思,只注意到了最后一段:

    If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

    于是帮fetch 加上了no-cors 的mode:

    fetch('http://localhost:3000' , { 
      mode: 'no-cors'
    }).then( res => console.log(res))
    

    改完之后重新整理,发现没有错误了,可是印出来的response 长得特别奇怪:

    没有任何资料,而且status 居然是0。小明在这之后debug 很久,找不出原因,不知道为什么就是拿不到资料。眼看死线将近,小明鼓起勇气去求助了前辈小华,小华跟他说:

    这是当然的啊,no-cors是个很容易误导初学者的参数,他的意思并不是「绕过cors拿到资料」,而是「我知道它过不了cors,但我没差,所以不要给我错误也不要给我response」

    你这问题一定要透过后端去解,我帮你跟后端说一声吧

    小华前辈不愧资深,三两下就解决了小明的问题。而后端那边也帮忙加上了一个header:Access-Control-Allow-Origin: *,代表来自任何origin的网站都可以用AJAX存取这个资源。

    后端程式码:

    app.get( '/' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.json({ 
        data: db.getFormOptions(), 
      }) 
    })
    

    小明把原本的mode 拿掉,改成:

    fetch( 'http://localhost:3000' ) 
      .then( res => res.json()) 
      .then( res =>  console .log(res))
    

    打开了浏览器,发现可以成功拿到选项了,也从network tab 里面看到了新增加的header:

    拿到资料以后,就只剩下把选项放上去画面而已,大概又半天的时间,小明就把这个功能做完并且测试完了,感谢小华前辈的帮助。

    Day1 总结

    mode: 'no-cors' 跟你想的不一样,这个没有办法解决CORS 问题。

    碰到CORS问题的时候,先确认后端有没有给你Access-Control-Allow-Origin这个header,没有的话请后端给你,否则你怎么试都不会过。

    Access-Control-Allow-Origin的值可以带*,代表wildcard,任何origin都合法,也可以带origin像是,代表只有这个origin是合法的。

    如果想带多个的话呢?抱歉,没有办法,就是只能全部都给过或者是给一个origin。因此也有后端会根据request的origin来决定response的Access-Control-Allow-Origin值会是多少,这个我们之后会再提到。

    Day2:不简单的CORS

    隔了一天之后,主管跟小明说更上层的人不满意这个使用者体验,送出表单之后要等个一两秒才能看到成功的画面,而且这中间也没有loading 什么的,体验不好,希望能改成AJAX 的做法送出表单而不是换页,就可以改善使用者体验。

    为了因应这个改变,后端又多出了一个API:POST /form,而且这次后端已经很自动地把Access-Control-Allow-Origin的header加上去了:

    app.post( '/form' , (req, res) => { 
        res.header( 'Access-Control-Allow-Origin','*' ) //省略写到db的程式码
        res.json({success:true}) 
    })
    

    小明之前已经做过类似的事情,因此很快就把程式码写好了:

    document .querySelector( '.contact-us-form' ) 
      .addEventListener( 'submit' , (e) => { //阻止表单送出
      e.preventDefault()
       //设置参数
       var data = new URLSearchParams();     data.append( 'email' , ' test@test.com ' )     data.append( 'source' , 'search' )
       //送出request
       fetch( 'http://localhost:3000/form' , { 
          method: 'POST' , 
          headers: { 'Content-Type':'application/x-www-form-urlencoded'},       
          body: data
       }).then( res => res.json()).then( res => console .log(res))   
    })
    

    测试之后也没有问题,正当小明要跟主管报告做好的时候,后端走过来跟小明说:「不好意思,我们后端最近做了一些改动,未来要统一改成用JSON 当作资料格式,所以你那边也要改一下,要送JSON 过来而不是urlencoded 的资料」

    小明听了之后心想:「这简单嘛,不就是改一下资料格式吗?」,于是改成这样:

    document .querySelector( '.contact-us-form' ) 
      .addEventListener( 'submit' , (e) => { 
        //阻止表单送出
        e.preventDefault()
        //设置参数
        var data = {       
          email: 'test@test.com' ,       
          soruce: 'search'     
        }
        //送出request
        fetch( ' http://localhost:3000/form ' , { 
          method: 'POST' , 
          headers: { 'Content-Type' : 'application/json'       },       
          body: JSON .stringify(data)     
        } ).then( res => res.json()).then( res => console .log(res))  
    })
    

    就只是换一下资料格式而已,改成用JSON 的方式传资料到后端。改完之后小明再测试了一遍,发现这一次居然挂掉了,而且出现错误讯息:

    Access to fetch at ' http://localhost:3000/form ' from origin 'null' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

    切到network tab 去看request 的状况,发现除了原本预期的POST 以外,还多了一个OPTIONS 的request:

    小明上网用错误讯息给的关键字:preflight request找了一下资料,发现CORS没有他想像中的简单。

    原来之前发送的那些请求都叫做「简单请求」,只要method是GET、POST或是HEAD然后不要带自订的header,Content-Type也不要超出:application/x-www-form-urlencoded、multipart/form-data或是text/plain这三种,基本上就可以被视为是「简单请求」(更详细的定义下一篇会说)。

    一开始串API的时候没有碰到错误,是因为Content-Type是application/x-www-form-urlencoded,所以被视为是简单请求。后来改成application/json就不符合简单请求的定义了,就变成是「非简单请求」。

    那非简单请求会怎么样呢?会多送出一个东西,叫做preflight request,中文翻作「预检请求」。这个请求就是小明在network tab 看到的那个OPTIONS 的request,针对这个request,浏览器会帮忙带上两个header:

    • Access-Control-Request-Headers

    • Access-Control-Request-Method

    以刚刚我们看到的/form的preflight request来说,内容是:

    • Access-Control-Request-Headers: content-type

    • Access-Control-Request-Method: POST

    前者会带上不属于简单请求的header,后者会带上HTTP Method,让后端对前端想送出的request 有更多的资讯。

    如果后端愿意放行,就跟之前一样,回一个Access-Control-Allow-Origin就好了。知道这点以后,小明马上请后端同事补了一下,后端程式码变成:

    app.post( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.json({ 
        success: true
       }) 
    })
    //多加这个,让preflight通过
    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.end() 
    })
    

    改好以后小明重新试了一下,发现居然还是有错误:

    Access to fetch at ' http://localhost:3000/form ' from origin 'null' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

    当你的CORS request含有自订的header的时候,preflight response需要明确用Access-Control-Allow-Headers来表明:「我愿意接受这个header」,浏览器才会判断预检通过。

    而在这个案例中,content-type就属于自订header,所以后端必须明确表示愿意接受这个header:

    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.header( 'Access-Control-Allow-Headers' , ' content-type' ) 
      res.end() 
    })
    

    如此一来,小明那边就可以顺利通过preflight request,只有在通过preflight 之后,真正的那个request 才会发出。

    流程会像是这样:

    • 我们要送出POST的request到http://localhost:3000/form

    • 浏览器发现是非简单请求,因此先发出一个preflight request

    • 检查response,preflight 通过

    • 送出POST的request到http://localhost:3000/form

    所以如果preflight 没有过,第一个步骤的request 是不会被送出的。

    经历过一番波折之后,这个改动总算也顺利完成了。现在我们可以成功在前端用AJAX 的方式送出表单资料了。

    Day2 总结

    CORS request分成两种:简单请求与非简单请求,无论是哪一种,后端都需要给Access-Control-Allow-Origin这个header。而最大的差别在于非简单请求在发送正式的request之前,会先发送一个preflight request,如果preflight没有通过,是不会发出正式的request的。

    针对preflight request,我们也必须给 Access-Control-Allow-Origin这个header才能通过。

    除此之外,有些产品可能会想要送一些自订的header,例如说X-App-Version好了,带上目前网站的版本,这样后端可以做个纪录:

    fetch( ' http://localhost:3000/form' , { 
          method: 'POST' , 
          headers: { 'X-App-Version' : "v0.1" , 'Content-Type' : 'application/json'} ,       
          body: JSON .stringify(data)     
    }).then( res => res.json()).then( res => console .log(res))
    

    当你这样做以后,后端也必须新增Access-Control-Allow-Headers,才能通过preflight:

    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.header( 'Access-Control-Allow-Headers' , ' X-App-Version, content-type' ) 
      res.end() 
    })
    

    简单来说,preflight 就是一个验证机制,确保后端知道前端要送出的request 是预期的,浏览器才会放行。我之前所说的「跨来源请求挡的是response 而不是request」,只适用于简单请求。对于有preflight 的非简单请求来说,你真正想送出的request 确实会被挡下来。

    那为什么会需要preflight request 呢?这边可以从两个角度去思考:

    • 相容性

    • 安全性

    针对第一点,你可能有发现如果一个请求是非简单请求,那你绝对不可能用HTML的form元素做出一样的request,反之亦然。举例来说, <form>的enctype不支援application/json,所以这个content type是非简单请求;enctype支援multipart/form,所以这个content type属于简单请求。

    对于那些古老的网站,甚至于是在XMLHttpRequest出现之前就存在的网站,他们的后端没有预期到浏览器能够发出method是DELETE或是PATCH的request,也没有预期到浏览器会发出content-type是application/json的request,因为在那个时代 <form><img>等等的元素是唯一能发出request的方法。

    那时候根本没有fetch,甚至连XMLHttpRequest 都没有。所以为了不让这些后端接收到预期外的request,就先发一个preflight request 出去,古老的后端没有针对这个preflight 做处理,因此就不会通过,浏览器就不会把真正的request 给送出去。

    这就是我所说的相容性,通过预检请求,让早期的网站不受到伤害,不接收到预期外的request。

    而第二点安全性的话,还记得在第一篇问过大家的问题吗?送出POST request 删除文章的那个问题。删除的API 一般来说会用DELETE 这个HTTP method,如果没有preflight request 先挡住的话,浏览器就会真的直接送这个request 出去,就有可能对后端造成未预期的行为(没有想到浏览器会送这个出来)。

    所以才需要preflight request,确保后端知道待会要送的这个request 是合法的,才把真正的request 送出去。

    Day3:带上Cookie

    昨天改的那版受到上层的极力赞赏,主管也请小明跟小华喝了手摇饮来庆祝。只是正当他们开心之时,行销部门的人跑来了,问说:「为什么这些request 都没有cookie?我们需要使用者的cookie 来做分析,请把这些cookie 带上」。

    此时小明才突然想起来:「对欸,跨来源的请求,预设是不会带cookie的」,查了一下MDN之后,发现只要带:credentials: 'include'应该就行了:

    fetch( ' http://localhost:3000/form' , { 
      method: 'POST' , 
      credentials: 'include' , //新增这个
      headers: { 'Content-Type' : 'application/json'   },   
      body: JSON .stringify(data) }
    ).then( res => res.json()).then( res => console .log(res))
    

    可是没想到前端却出现了错误讯息:

    Access to fetch at ' http://localhost:3000/form ' from origin ' http://localhost:8080 ' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the ' Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

    错误讯息其实已经解释得很清楚了,如果要带上cookie的话,那Access-Control-Allow-Origin不能是*,一定要明确指定origin。

    为什么会这样呢?因为如果没有这个限制的话,那代表任何网站(任何origin)都可以发request 到这个API,并且带上使用者的cookie,这样就会有安全性的问题产生,大概就跟CSRF 有异曲同工之妙。

    所以因为安全性的关系,强制你如果要带上cookie,后端一定要明确指定是哪个origin有权限。除此之外,后端还要额外带上Access-Control-Allow-Credentials: true这个header。

    于是小明再度请小华改一下后端:

    const VALID_ORIGIN = ' http :// localhost : 8080 '
     app.post( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) //明确指定
      res .header( 'Access-Control-Allow-Credentials' , true ) //新增这个
      res.json({ 
        success: true
       }) 
    })
    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) //明确指定
      res.header( 'Access-Control-Allow-Credentials' , true ) //新增这个
      res.header( 'Access-Control-Allow-Headers' , 'content-type, X-App-Version' ) 
      res.end() 
    })
    

    改完之后的版本明确指定才有权限存取CORS Response,也加上了这个header。http://localhost:8080Access-Control-Allow-Credentials

    如此一来就大功告成了,在发送request 的时候可以成功带上Cookie,行销部门那边的需求也搞定了,耶依。

    Day3 总结

    如果你需要在发送request 的时候带上cookie,那必须满足三个条件:

    • 后端Response header 有 Access-Control-Allow-Credentials: true

    • 后端Response header的Access-Control-Allow-Origin不能是*,要明确指定

    • 前端fetch 加上 credentials: 'include'

    这三个条件任何一个不满足的话,都是没办法带上cookie 的。

    除了这个之外还有一件事情要特别注意,那就是不只带上cookie,连设置cookie也是一样的。后端可以用Set-Cookie这个header让浏览器设置cookie,但一样要满足上面这三个条件。如果这三个条件没有同时满足,那尽管有Set-Cookie这个header,浏览器也不会帮你设置,这点要特别注意。

    事实上呢,无论有没有想要存取Cookie,都会建议 Access-Control-Allow-Origin不要设定成*而是明确指定origin,避免预期之外的origin跨站存取资源。若是你有多个origin的话,建议在后端有一个origin的清单,判断request header内的origin有没有在清单中,有的话就设定Access-Control-Allow-Origin,没有的话就不管它。

    Day4:存取自订header

    还记得我们一开始串的那一个API 吗?跟后端拿选项的API。虽然之前已经顺利完成,但没想到有陨石砸下来了。今天早上上面说要加一个新的需求。

    这个需要是要对这个API的内容做版本控制,后端会在response header里面多带上一个header:X-List-Version,来让前端知道这个选项的清单是哪一个版本。

    而前端则是要拿到这个版本,并且把值放到表单里面一起送出。

    后端会像是这样:

    app.get( '/' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.header( 'X-List-Version' , '1.3' ) 
      res.json({ 
        data: [ 
          { name : '1/10活动' , id : 1 }, 
          { name : '2/14特别活动' , id : 2 } 
        ] 
      }) 
    })
    

    由于这一个API 的内容本来就是公开的,所以没有允许特定的origin 也没有关系,可以安心使用wildcard。

    小明把之前的程式码改了一下,试着把header 先列印出来看看:

    fetch( ' http://localhost:3000' ) 
      .then( res => { console .log(res.headers.get( 'X-List-Version' )) return res.json()   })   
      .then( res => console .log(res))
    

    此时,神奇的事情发生了。明明从network tab 去看,确实有我们要的response header,但是在程式里面却拿不到,输出null。小明检查了几遍,确定字没打错,而且没有任何错误讯息,但就是拿不到。

    卡了一个小时之后,小明决定再次求助前辈小华。小华身为资深前辈,一看到这个状况之后就说了:

    如果你要存取CORS response的header,尤其是这种自定义的header的话,后端要多带一个Access-Control-Expose-Headers的header喔,这样前端才拿得到

    「原来是这样吗!」小明恍然大悟,去找了后端的同事,让他加上这个header:

    app.get( '/' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , '*' ) 
      res.header( 'Access-Control-Expose-Headers' , 'X -List-Version' ) //加这个
      res.header( 'X-List-Version' , '1.3' ) 
      res.json({ 
        data: [ 
          { name : '1/10活动' , id : 1 }, 
          { name : '2/14特别活动' , id : 2 } 
        ] 
      }) 
    })
    

    改完之后小明再测试一遍,发现果真可以正确拿到header 了!感恩小华,赞叹小华,平安的一天又度过了。

    Day4 总结

    当你拿到跨来源的response的时候,基本上都可以拿到response body,也就是内容。但是header就不一样了,只有几个基本的header可以直接拿到,例如说Content-Type就是一个。

    除此之外,如果你想拿其他header,尤其是自定义的header的话,后端就需要带上Access-Control-Expose-Headers,让浏览器知道说:「我愿意把这个header开放出去让JS看到」,这样子前端才能顺利抓到header。

    如果没有加的话就会拿到null,就跟这个header 不存在一样。

    Day5:编辑资料

    原本以为一切都很顺利的小明又再次踢到了铁板。这次是老板那边提出的需求,现在一送出表单之后就没机会再更改了,若是使用者意识到哪边有填错,就只能重新再填一遍。而老板觉得这样的体验不好,希望在使用者送出表单以后还有一次机会能够挽回,可以编辑刚刚送出的表单。

    跟后端讨论过后,在送出表单之后后端会给一个token,前端只要带着这个token去打PATCH /form这个API,就能够编辑刚刚表单的内容。

    后端长得像这样,一样有把该加的header 都加好:

    const VALID_ORIGIN = ' http :// localhost : 8080 '
    app.patch( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) 
      res.header( ' Access-Control-Allow-Credentials' , true ) 
      //省略编辑的部分
      res.json({     success: true   }) 
    })
    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) 
      res.header( 'Access-Control-Allow-Credentials' , true ) 
      res .header( 'Access-Control-Allow-Headers' , 'content-type, X-App-Version' ) 
      res.end() 
    })
    

    而小明立刻开始着手前端的部分,大概像是这样:

    fetch( 'http://localhost:3000/form' , { 
      method: 'PATCH' , 
      credentials: 'include' , 
      headers: { 'X-App-Version' : "v0.1" , 'Content-Type' : 'application/json'   },   
      body: JSON .stringify({     
        token: 'test_token' ,     
        content: 'new content'   
      }) 
    }).then( res => res.json()).then( res =>console.log(res))
    

    其实跟之前送出表单的程式码八七分像,差别大概只在body 跟method 的部分。然而,小明在测试的时候,浏览器又跳出错误了:

    Access to fetch at ' http://localhost:3000/form ' from origin ' http://localhost:8080 ' has been blocked by CORS policy: Method PATCH is not allowed by Access-Control-Allow-Methods in preflight response.

    跨来源的请求只接受三种HTTP Method:GET、HEAD以及POST,除了这三种之外,都必须由后端回传一个Access-Control-Allow-Methods,让后端决定有哪些method可以用。

    因此后端要改成这样:

    // preflight
     app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) 
      res.header( 'Access-Control-Allow-Credentials' , true ) 
      res.header( 'Access-Control-Allow-Methods' , 'PATCH' ) //多这个
      res.header( 'Access-Control-Allow-Headers' , 'content-type, X-App-Version' ) 
      res.end() 
    })
    

    如此一来,浏览器就知道前端能够使用PATCH 这个method,就不会把后续的request 给挡下来了。

    Day5 总结

    如果前端要使用GET、HEAD以及POST以外的HTTP method发送请求的话,后端的preflight response header必须有Access-Control-Allow-Methods并且指定合法的method,preflight才会通过,浏览器才会把真正的request发送出去。

    这个就跟前面提过的Access-Control-Allow-Headers有点像,只是一个是在规范可以用哪些method,一个是在规范可以用哪些request headers。

    Day6:快取preflight request

    好不容易满足了公司各个大头的需求,没想到在上线前夕,技术这端出问题了。小明原本以为解掉了所有跨来源的问题就行了,可是却忽略了一个地方。在QA 对网站做压测的时候,发现preflight request 的数量实在是太多了,而且就算同一个使用者已经预检过了,每次都还是需要再检查,其实满浪费效能的。

    于是QA 那边希望后端可以把这个东西快取住,这样如果同一个浏览器重复发送request,就不用再做预检。

    虽然说小明是做前端的,但他其实想成为CORS大师,于是就跟后端一起研究该怎么解决这个问题。最后他们找到了一个header:Access-Control-Max-Age,可以跟浏览器说这个preflight response能够快取几秒。

    接着后端把这个header 加上去:

    app.options( '/form' , (req, res) => { 
      res.header( 'Access-Control-Allow-Origin' , VALID_ORIGIN) 
      res.header( 'Access-Control-Allow-Credentials' , true ) 
      res .header( 'Access-Control-Allow-Headers' , 'content-type, X-App-Version' ) 
      res.header( 'Access-Control-Max-Age' , 300 ) 
      res.end() 
    })
    

    这样preflight response 就会被浏览器快取300 秒,在300 秒内对同一个资源都不会再打到后端去做preflight,而是会直接沿用快取的资料。

    总结

    让我们一个一个来回忆故事中出现的各个header。

    一开始小明需要存取跨来源请求的response,因此需要后端协助提供Access-Control-Allow-Origin,证明这个origin是有权限的。

    再来因为要带自订的header,所以后端要提供Access-Control-Allow-Headers,写明client可以带哪些header上去。同时也因为多了preflight requset,后端要特别处理OPTIONS的request。

    然后我们需要用到cookie,所以Access-Control-Allow-Origin不能是*,要改成单一的origin。而后端也要多提供Access-Control-Allow-Credentials: true。

    接着前端需要存取header,所以后端必须提供Access-Control-Expose-Headers,跟浏览器说前端可以拿到哪些header。而前端如果要使用HEAD、GET跟POST之外的method,后端要加上Access-Control-Allow-Methods。

    关于快取的部分,则是用Access-Control-Max-Age。

    整串故事看下来,其实你会发现根本没什么前端的事情。前端在整个故事中担任的角色就是:写code => 发现错误=> 回报后端=> 后端修正=> 完成功能。这也呼应了我之前一再强调的:「CORS 的问题,通常都不是前端能解决的」。

    说穿了,CORS 就是藉由一堆的response header 来跟浏览器讲说哪些东西是前端有权限存取的。如果没有后端给的这些header,那前端根本什么也做不了。因此无论是前端还是后端,都有必要知道这些header,未来碰到相关问题的时候才知道怎么解决。

    顺带一提,我觉得Chrome 的错误提示好像愈做愈棒了,印象中以前好像没有讲得那么详细,现在详细到爆,甚至可以直接看错误讯息而不Google 就知道该怎么修。

    希望透过这一篇,能让大家理解CORS 有哪些response header,以及什么是preflight request,在哪些情形之下会触发。理解这些以后,你对整个CORS protocol 的理解大概就有八成了。

    作者:@huli 原文:https://blog.huli.tw/2021/02/19/cors-guide-3/

    声明:文章著作权归作者所有,如有侵权,请联系小编删除。

    最后

    欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

    回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

    回复「交流」,吹吹水、聊聊技术、吐吐槽!

    回复「阅读」,每日刷刷高质量好文!

    如果这篇文章对你有帮助,「在看」是最大的支持

     》》面试官也在看的算法资料《《

    “在看和转发”就是最大的支持

    展开全文
    lunahaijiao 2021-07-04 00:24:02
  • 本文为译文。简介APIS是可以将富网页应用串连在一起的线程。但是这个应用难以转给浏览器,跨域请求技术的选择被限制了,类似JSONP...通过设置XMLHttpRequest的头部,CORS允许开发者使用类似同域中请求惯用的方法...

    本文为译文。

    简介

    APIS是可以将富网页应用串连在一起的线程。但是这个应用难以转给浏览器,跨域请求技术的选择被限制了,类似JSONP(由于安全考虑,使用会被限制),或者配置代理(设置和维护都比较头痛)。

    Cross-Origin Resource Sharing(CORS)是允许来自浏览器的跨域通信的W3C规范。通过设置XMLHttpRequest的头部,CORS允许开发者使用类似同域中请求惯用的方法。

    CORS的用法很简单,假设网站alice.com有一些bob.com想要获取的数据。这类型的请求以前通常在浏览器的同源策略下不被允许。但是,借助CORS请求,alice.com可以加一些特殊回应头信息允许bob.com取得数据。

    可以从这个例子里面看出来,支持CORS需要服务器和客户端之间的协调。幸运的是,如果你是客户端开发者,你可以屏蔽掉这中间大多数的细节。这篇文章余下的部分会展示客户端如何进行跨域请求,及服务器如何配置来支持CORS。

    发起CORS请求

    这部分介绍如何使用javascript发起CORS请求。

    创建XMLHttpRequest请求:

    下列浏览器支持CORS:

    Chrome 3+

    Firefox 3.5+

    Opera 12+

    Safari 4+

    Internet Explorer 8+

    Chrome,Firefox,Opera,Safari都使用XMLHttpRequest2对象,IE使用类似的XDomainRequest对象,与他本身对应的XMLHttpRequest对象工作机制基本相同,但是另外增加了安全防范。

    首先要创建合适的请求对象,像这样:

    事件处理

    原始的XMLHttpRequest只有一个事件处理onreadystatechange,来处理所有的响应。虽然onreadystatechange依然可以使用,XMLHttpRequest2引进了一系列新的处理,下面是一个列表:

    在大多数情况下,你只仅仅想处理onload和onerror事件:

    当有错误的时候,浏览器不会报告是什么错误。例如,FF对所有错误报告一个状态0和空statusText。浏览器也会向控制输出台报错,但是这个消息没法被javascript获取到。当处理onerror的时候,会知道有一个错误发生了,但没有更多信息了。

    withCredentials

    标准的CORS在默认情况下不发送和设置任何cookie。为了使cookie成为请求的一部分包含进来,需要设置XMLHttpRequest的withCredentials属性为true:

    为了达成这个目的,服务器也需要设置Access-Control-Allow-Credientials为true来允许认证。

    withCredentials属性会包含来自远程域的请求的任何cookie。注意这些cookies仍然遵循同源策略,所以你的javascript代码仍然从document.cookie中访问不到这些cookies或者请求头.他们只能被远程域所控制。

    发送请求

    现在你的CORS请求已经配置好了,你准备发送请求。这个通过调用send()方法来实现:

    如果请求包含请求体,可以被指定为一个参数给send().

    就是酱紫!假定服务器已经完全配置好来响应CORS请求了,你的onload处理会收到响应,就像你十分熟悉的标准的同域XHR那样。

    最后最后一个例子

    这是一个完整的CORS请求例子。运行下例子在浏览器debugger里面看看发送的请求。

    在服务器端添加CORS支持

    多数的CORS重担在浏览器和服务器之间被处理了。浏览器增加了额外的一些头,有时发起额外的请求,在CORS请求期间。这些额外的请求对于客户端是不可见的(可以使用Wireshark等工具来捕获)。

    浏览器厂商对于浏览器端的实现是有责任的。下面了解下服务器端如何配置来支持CORS。

    请求的类型

    跨域请求分类:

    1.简单请求

    2.非简单请求

    简单请求不需要使用CORS从浏览器发出请求。例如,一个JSONP跨域请求可以是一个跨域的GET请求。

    非简单请求,在浏览器与服务器间需要一点额外的交互。

    处理一个简单请求

    以一个客户端的简单请求为例。下面代码展示了javascript的一个GET请求,以及浏览器发送的实际请求。CORS中特殊的头粗体显示。

    首先需要注意的是,一个合法的CORS请求总是包含Origin头。这个Origin头是浏览器加上去的,并且不能被用户控制。这个头的值是来自请求源的协议(例如http),域(例如bob.com),和端口(只在不是默认端口的情况下包含,例如81);例如http://api.alice.com.

    Origin头的存在不意味着请求是一个跨域请求。虽然所有跨域的请求都会包含Origin头,一些同域请求同样也会包含Origin头。例如,火狐在同域请求中不包含Origin头。但是Chrome和Safari在同域的POST/PUT/DELETE请求(同域的GET请求不会有Origin头)中会包含Origin头。下面有一个同域请求包含Origin头的例子:

    值得庆幸的是浏览器在同域请求时不会期望CORS请求头。同域请求的响应发送给用户,不管是否有CORS请求头。然而,如果服务器代码返回了如果Origin不匹配允许的域列表的错误,确保包含请求的来源域。

    所有CORS相关的的头都是Access-Control为前缀的。下面是每个头的一些细节。

    Access-Control-Allow-Origin(必须)-这个头在所有合法的CORS响应中必须被包含;发送头会引起CORS请求失败。这个头的值可以是Origin请求头的回应(就像上面的例子里面一样),或者是允许来自任何域请求的"*"。如果你希望任何站点都可以获取数据,使用"*"就很好。但是如果你希望更好的控制谁可以获取你的数据,在头中使用一个实际的值。

    Access-Control-Allow-Credentials(可选)-默认情况下,cookies在CORS请求中不被包含。使用这个头意味着在CORS请求中应当包含cookies.这个头的唯一合法值是true(全部小写)。如果不需要cookies,不要包含这个头(而不是把值设置为false).

    Access-Control-Allow-Credentials头与XMLHttpRequest2的withCredentials属性结合使用。所有这些属性都必须设置为true以便于CORS请求能够成功。如果withCredentials是true,但是没有Access-Control-Allow-Credentials头,请求会失败(反之亦然)。

    上面提到过,除非你确定需要cookies包含在请求中,否则不设置这个头。

    Access-Control-Expose-Headers(可选)-XMLHttpRequest2对象有一个返回特别响应头值的getResponseHeader()方法。在CORS请求中,getResponseHeader()只能获取简单的响应头。简单响应头定义如下:

    如果希望客户端获取其他头,必须用Access-Control-Expose-Headers头。这个头的值是一个逗号分隔的想要公开给客户端的响应头列表。

    处理非简单请求

    上面就是如何处理简单GET请求,但如果想要做更多的事情要怎么做?也许你想要支持其他HTTP动作,像PUT或DELETE,或者想要使用Content-Type:application/json支持JSON.你就需要处理所谓非简单请求。

    非简单请求看上去像到客户端的一个单一请求,但是实际上在遮罩下包含了两个请求。浏览器首先发出一个预先请求,就好像询问服务器应允进行实际请求。一旦被允许,浏览器发送实际请求。浏览器透明的处理这两个请求的细节。预响应也可以被缓存下来以便不需要在每个请求前都发送。

    下面是一个非简单请求的例子:

    像简单请求一样,浏览器给每个请求加上Origin头,包含预请求。预请求像HTTP OPTIONS请求一样发出(所以确保服务器可以响应这个方法).他也包含额外的一些头:

    Access-Control-Request-Method-HTTP实际请求的方法。这个请求头总是被包含,即使HTTP请求是一个简单请求,就像早些定义的那些(GET,POST,HEAD).

    Access-Control-Request-Headers-一个在请求中包含的非简单头的逗号分隔的列表。

    预请求是在发送实际请求前,问询服务器是否允许实际请求的方式。服务器应当检查上面的两个头来确认HTTP方法和请求头是合法的及可以接受的。

    如果HTTP请求方法和头是合法的,服务器会如下响应:

    Access-Control-Allow-Origin(必须)-如简单响应一样,预响应必须包含这个头。

    Access-Control-Allow-Method(必须)-所支持的HTTP方法逗号分隔的列表。注意虽然预请求只是询问HTTP方法是否允许,响应头可以包含所有支持的HTTP方法。这个是有帮助的,因为预响应可能会被缓存,所以单一的预响应可以包含多种请求类型的细节。

    Access-Control-Allow-Headers(如果请求头包含Access-Control-Allow-Headers就必须包含)-逗号分隔的所支持的请求头列表。像上面的Access-Control-Allow-Methods一样,这个可以列出服务器支持的所有头(不仅仅是在预请求中请求的头)。

    Access-Control-Allow-Credentials(可选)-同简单请求。

    Access-Control-Max-Age(可选)-在每个请求上进行预请求是代价很高的,因为浏览器对于客户端的请求会进行两次请求。这个头的值允许预响应缓存一个指定的秒数。

    一旦预请求给到了应允,浏览器发送实际请求。实际请求看起来像简单请求,响应以同样的方式处理:

    如果服务器想否决CORS请求,可以只返回一个通用的响应(像HTTP 200),不带任何CORS请求头。如果预请求时的HTTP方法或者头不合法,服务器可能想要否决请求。由于在响应中没有CORS特定的头,浏览器断定请求是不合法的,不发送实际请求:

    如果在CORS请求中有错误,浏览器会触发客户端的onerror事件处理。也会在控制台上打印下面的错误:

    浏览器不会给为什么错误会发生的细节信息,只告诉你出了些问题。

    关于安全

    虽然CORS奠定了跨域请求的基础工作,CORS头不是健全的安全实践的替代。在站点上不能够依赖CORS头保护资源安全。使用CORS头给予浏览器跨域请求的接入,但是使用其他的安全手段,像cookies或者OAuth2,如果需要额外的安全限制的话。

    来自jQuery的CORS

    jQuery的$.ajax()方法可以用来进行常规XHR请求和CORS请求。一点点jQuery相关实现的笔记:

    jQuery的CORS实现不包含IE的XDomainRequest对象。但是有jQuery插件可以用这个。可以在http://bugs.jquery.com/ticket/8283上查看细节。

    如果浏览器支持CORS的话(在IE中会返回false,看下上条说明),$.support.cors布尔量会被设置成true。这是检查是否支持CORS的快速方法。

    下面是一个用jQuery进行CORS请求的例子。注释给出了确切的属性如何与CORS交互。

    Chrome扩展件的跨域

    Chrome扩展件以两种不同的方式支持跨域:

    在manifest.json中包含域-Chrome扩展件可以向任何域发送跨域请求,如果域被包含在manifest.json文件的permissions部分:

    服务器不需要包含任何额外的CORS头或者做另外的工作来使请求成功。

    CORS请求-如果域不在manifest.json文件中,Chrome扩展件进行一个标准的CORS请求。Origin头的值是"chrome-extension://[CHROME EXTENSION ID]".这意味着来自Chrome扩展件的请求受本文提到的同样的CORS请求规则的约束。

    CORS w/图片

    在Canvas和WebGL上下文中,跨域图片可以造成很大的问题。可以在img元素上使用crossOrigin属性。

    CORS服务器流程图

    下面的流程图说明了服务器如何确定在CORS响应中需要加哪些头。

    --end--

    展开全文
    weixin_39863008 2020-12-19 05:05:09
  • CORS CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,...

    一 跨域

    同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

    同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

    简单请求跨域

    我们创建两个django项目,第一个叫做s1,一个叫做s2,s1用8000端口启动,s2用8001端口启动

    s1项目的index.html文件内容如下:

    Title

    s1的首页

    Ajax请求

    $('#btn').click(function () {

    $.ajax({

    //url:'/books/', 访问自己服务器的路由,同源(ip地址、协议、端口都相同才是同源)

    url:'http://127.0.0.1:8001/books/', //访问其他服务器的路由,不同源,那么你可以访问到另外一个服务器,但是浏览器将响应内容给拦截了,并给你不同源的错误:Access to XMLHttpRequest at 'http://127.0.0.1:8001/books/' from origin 'http://127.0.0.1:8000' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'http://127.0.0.1:8002' that is not equal to the supplied origin.

    #并且注意ip地址和端口后面是一个斜杠,如果s2的这个url没有^books的^符号,那么可以写两个//。       type:'get',

    success:function (response) {

    console.log(response);

    }

    })

    })

    urls.py文件内容如下:

    from django.conf.urls import url

    from django.contrib import admin

    from app01 import views

    urlpatterns = [

    url(r'^admin/', admin.site.urls),

    url(r'^index/', views.index),

    url(r'^books/', views.books),

    ]

    views.py内容如下:

    from django.shortcuts import render,HttpResponse

    from django.http import JsonResponse

    # Create your views here.

    def index(request):

    return render(request,'index.html')

    def books(request):

    # return JsonResponse(['西游记','三国演义','水浒传'],safe=False)

    obj = JsonResponse(['西游记','三国演义','水浒传'],safe=False)

    return obj

    s2项目的urls.py内容如下:

    from django.conf.urls import url

    from django.contrib import admin

    from app01 import views

    urlpatterns = [

    url(r'^admin/', admin.site.urls),

    url(r'^books/', views.books),

    ]

    views.py内容如下:

    from django.shortcuts import render

    from django.http import JsonResponse

    # Create your views here.

    def books(request):

    # return JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    obj = JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    #下面这个响应头信息是告诉浏览器,不要拦着,我就给它,"*"的意思是谁来请求我,我都给

    # obj["Access-Control-Allow-Origin"] = "*"

    obj["Access-Control-Allow-Origin"] = "http://127.0.0.1:8000" #只有这个ip和端口来的请求,我才给他数据,其他你浏览器帮我拦着

    return obj

    以上是一个简单请求的跨域问题和解决方法。

    二 CORS

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

    整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

    因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

    浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

    只要同时满足以下两大条件,就属于简单请求。

    (1) 请求方法是以下三种方法之一:(也就是说如果你的请求方法是什么put、delete等肯定是非简单请求)

    HEAD

    GET

    POST

    (2)HTTP的头信息不超出以下几种字段:(如果比这些请求头多,那么一定是非简单请求)

    Accept

    Accept-Language

    Content-Language

    Last-Event-ID

    Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain,也就是说,如果你发送的application/json格式的数据,那么肯定是非简单请求,vue的axios默认的请求体信息格式是json的,ajax默认是urlencoded的。

    凡是不同时满足上面两个条件,就属于非简单请求。

    我们改一下上一节的s1项目的index.html文件中的ajax里面的内容:

    Title

    s1的首页

    Ajax请求

    $('#btn').click(function () {

    $.ajax({

    url:'http://127.0.0.1:8001/books/',

    type:'post',

    contentType:'application/json',//非简单请求,会报错:Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

    data:JSON.stringify({

    a:'1'

    }), //headers:{b:2},

    success:function (response) {

    console.log(response);

    }

    })

    })

    浏览器对这两种请求的处理,是不一样的。

    * 简单请求和非简单请求的区别?

    简单请求:一次请求

    非简单请求:两次请求,在发送数据之前会先发一次请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。

    * 关于“预检”

    - 请求方式:OPTIONS

    - “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息

    - 如何“预检”

    => 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过

    Access-Control-Request-Method

    => 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过

    Access-Control-Request-Headers

    看图:

    s2项目的views.py内容如下:

    from django.shortcuts import render

    from django.http import JsonResponse

    # Create your views here.

    def books(request):

    # return JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    obj = JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    # obj["Access-Control-Allow-Origin"] = "*"

    obj["Access-Control-Allow-Origin"] = "http://127.0.0.1:8000"

    print(request.method)

    #处理预检的options请求,这个预检的响应,我们需要在响应头里面加上下面的内容

    if request.method == 'OPTIONS':

    # obj['Access-Control-Allow-Headers'] = "Content-Type" #"Content-Type",首字母小写也行

    # obj['Access-Control-Allow-Headers'] = "content-type" #"Content-Type",首字母小写也行。这个content-type的意思是,什么样的请求体类型数据都可以,我们前面说了content-type等于application/json时,是复杂请求,复杂请求先进行预检,预检的响应中我们加上这个,就是告诉浏览器,不要拦截

    obj['Access-Control-Allow-Headers'] = "content-type,b" #发送来的请求里面的请求头里面的内容可以定义多个,后端需要将头配置上才能访问

    return obj

    支持跨域,简单请求

    服务器设置响应头:Access-Control-Allow-Origin = '域名' 或 '*'

    支持跨域,复杂请求

    由于复杂请求时,首先会发送“预检”请求,如果“预检”成功,则发送真实数据。

    “预检”请求时,允许请求方式则需服务器设置响应头:Access-Control-Request-Method

    “预检”请求时,允许请求头则需服务器设置响应头:Access-Control-Request-Headers

    关于请求方式的跨域问题及解决方法:

    s1的index.html文件内容如下:

    Title

    s1的首页

    Ajax请求

    $('#btn').click(function () {

    $.ajax({

    url:'http://127.0.0.1:8001/books/',

    //type:'delete',

    //type:'post',

    type:'put',

    contentType:'application/json',//非简单请求,会报错:Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

    data:JSON.stringify({

    a:'1'

    }),

    headers:{b:'2'},

    success:function (response) {

    console.log(response);

    }

    })

    })

    s2项目的views.py内容如下:

    from django.shortcuts import render

    from django.http import JsonResponse

    # Create your views here.

    def books(request):

    # return JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    obj = JsonResponse(['西游记2','三国演义2','水浒传2'],safe=False)

    # obj["Access-Control-Allow-Origin"] = "*"

    obj["Access-Control-Allow-Origin"] = "http://127.0.0.1:8000"

    print(request.method)

    #处理预检的options请求,这个预检的响应,我们需要在响应头里面加上下面的内容

    if request.method == 'OPTIONS':

    obj['Access-Control-Allow-Headers'] = "content-type,b" #发送来的请求里面的请求头里面的内容可以定义多个,后端需要将头配置上才能访问

    obj['Access-Control-Allow-Methods'] = "DELETE,PUT" #通过预检的请求方法设置

    return obj

    展开全文
    weixin_29958569 2020-12-24 08:57:51
  • CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 本文详细介绍CORS的内部机制。 (图片...

    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

    它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

    本文详细介绍CORS的内部机制。

    (图片说明:摄于阿联酋艾因(Al Ain)的绿洲公园)

    一、简介

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

    整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

    因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

    二、两种请求

    浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

    只要同时满足以下两大条件,就属于简单请求。

    (1) 请求方法是以下三种方法之一:

    • HEAD
    • GET
    • POST

    (2)HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

    这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

    凡是不同时满足上面两个条件,就属于非简单请求。

    浏览器对这两种请求的处理,是不一样的。

    三、简单请求

    3.1 基本流程

    对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

    下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

    GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

    如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

    如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

    Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8

    上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

    (1)Access-Control-Allow-Origin

    该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

    (2)Access-Control-Allow-Credentials

    该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

    (3)Access-Control-Expose-Headers

    该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

    3.2 withCredentials 属性

    上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

    Access-Control-Allow-Credentials: true

    另一方面,开发者必须在AJAX请求中打开withCredentials属性。

    var xhr = new XMLHttpRequest(); xhr.withCredentials = true;

    否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

    但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

    xhr.withCredentials = false;

    需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

    四、非简单请求

    4.1 预检请求

    非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

    下面是一段浏览器的JavaScript脚本。

    var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();

    上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。

    浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

    OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

    除了Origin字段,"预检"请求的头信息包括两个特殊字段。

    (1)Access-Control-Request-Method

    该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

    (2)Access-Control-Request-Headers

    该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

    4.2 预检请求的回应

    服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

    HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain

    上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

    Access-Control-Allow-Origin: *

    如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

    XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

    服务器回应的其他CORS相关字段如下。

    Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000

    (1)Access-Control-Allow-Methods

    该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

    (2)Access-Control-Allow-Headers

    如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

    (3)Access-Control-Allow-Credentials

    该字段与简单请求时的含义相同。

    (4)Access-Control-Max-Age

    该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

    4.3 浏览器的正常请求和回应

    一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

    下面是"预检"请求之后,浏览器的正常CORS请求。

    PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...

    上面头信息的Origin字段是浏览器自动添加的。

    下面是服务器正常的回应。

    Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8

    上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

    五、与JSONP的比较

    CORS与JSONP的使用目的相同,但是比JSONP更强大。

    JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

    展开全文
    zhanghao143lina 2021-01-25 11:37:11
  • weixin_39954569 2021-08-08 08:56:14
  • weixin_35539198 2020-12-23 19:04:50
  • weixin_34489345 2021-01-17 02:53:33
  • MR_Peach07 2021-10-27 09:42:38
  • weixin_39190897 2021-02-09 13:34:31
  • m0_59508658 2021-08-19 10:11:57
  • zzuhkp 2021-10-07 00:25:04
  • weixin_31539461 2021-06-13 03:05:56
  • u014165119 2020-12-22 12:37:39
  • qq_36759224 2021-12-04 18:24:20
  • weixin_36364419 2021-08-07 00:37:28
  • weixin_42302384 2020-12-24 03:38:48
  • y534560449 2021-11-19 15:43:25
  • weixin_35723036 2021-02-12 10:21:16
  • weixin_42511217 2021-08-07 00:37:31
  • weixin_39613433 2020-12-28 23:38:35
  • superfjj 2021-09-13 09:47:28
  • qq_43477721 2021-11-08 23:56:19
  • weixin_39998273 2020-12-19 05:05:12
  • m_iNoError 2021-04-14 10:47:53
  • weixin_29455479 2020-12-23 19:05:02
  • weixin_39668282 2020-12-28 23:38:29
  • weixin_43847838 2021-08-21 16:18:27
  • weixin_32408723 2020-12-24 03:38:50
  • weixin_34353619 2021-04-13 00:31:33
  • weixin_46416295 2021-06-21 11:10:37

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 83,857
精华内容 33,542
关键字:

cors