前端开发中JSONP和cors的跨域解决方案

236 阅读9分钟

1.跨域产生的原因及意义

  1. 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域,跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。 浏览器为了安全起见设置了同源策略,当页面执行脚本的时候,浏览器会检查访问的资源是否同源,如果不是,就会报错。
    通俗点说就是从 A 向 B 发请求,如若他们的地址协议、域名、端口都不相同,直接访问就会造成跨域问题,跨域是非常常见的现象!请求是跨域的但并不一定会报错,普通的图片请求,css文件请求是不会报错的。报错的条件是浏览器的同源策略,且发送Ajax请求,跨域是客户端问题。

2.常见的跨域场景
1.)   同一域名下的不同文件或路径,允许访问。
www.domain.com/a.js
www.domain.com/b.js
www.domain.com/lab/c.js
2.)   同一域名下的不同端口, 不允许访问。
www.domain.com:8000/a.js
www.domain.com/b.js
3.)   同一域名下的不同协议, 不允许访问。
www.domain.com/a.js
www.domain.com/b.js
4.)   同一ip地址下的不同域名之间,不允许访问。
www.domain.com/a.js
http://192.168.4.12/b.js
5.)   不同域名之间不允许访问。
www.domain1.com/a.js
www.domain2.com/b.js

2.JSONP解决跨域

  1. JSONP(JSON with Padding)是 json 的一种"使用模式",可以让网页从别的域名(网站)那里获取资料,即跨域读取数据。它利用了script标签不存在域的限制实现跨域请求, 为了便于客户端使用数据,逐渐形成了一种非正式传输协议,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。 它不像Ajax请求那样受到同源策略的限制,兼容性很好,在老的浏览器中都可以运行,并且在请求完毕后可以通过调用callback的方式回传结果。
  2. JSONP的原理
    1、利用script标签的src属性来实现跨域。
    2、通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信。

通俗一点就是:
1、浏览器创建 script,src 指向服务器,同时传一个查询参数 ?callbackName=xxx
2、服务器根据查询参数callback,将传递过来的callback以及要返回的数据拼接成一个字符串,返回给浏览器
3、浏览器接收到响应,就会执行返回的函数
4、那么浏览器就得到了请求到的数据

浏览器的代码如下

<script>
     
    function success(data) {
        console.log('JSONP响应回来的数据');
        console.log(data);
    }
    </script>
    <script src="http://127.0.0.1:1001/test?callback=success&name=lv"></script>

服务器的代码如下:

const express=require('express')
app=express()
app.listen(1001,()=>{
    console.log('启动了')
})
app.get('/test',(req,res)=>{
    let {callback}=req.query
    console.log(callback)
    console.log(req.query)
    let result={
        code:0,
        data:'hello'
    }
    let str=`${callback}(${JSON.stringify(result)})`
    res.send(str)
})

(3) JSONP优缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全,只要服务器支持,谁都可以调用。

3.cors解决跨域

1.CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

2.过程分析:

  • 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求;若不同源,则发送跨域请求。
  • 当我们发起跨域请求时,如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是 OPTIONS 请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。
  • 服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含 Access-Control-Allow-origin 字段,若配置过域名,则返回 Access-Control-Allow-origin + 对应配置规则里的域名的方式。
  • 浏览器根据接收到的响应头里的 Access-Control-Allow-origin 字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

简单请求

不会发起cors预检请求的请求,称为简单请求。满足下列要求的请求都是简单请求:

  1. 使用下面的请求方式
    (1)GET
    (2)POST
    (3)HEAD
  2. 请求头中只包含
    (1)Accept
    (2)Accept-Language
    (3)Content-Language
    (4)ContentType
    text/plain
    multipart/form-data
    application/x-www-form-urlencoded

基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误。

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

   Access-Control-Allow-Origin:http://127.0.0.1:5500

如果是简单请求,浏览器代码如下:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script>
    axios.get('http://127.0.0.1:1002/test').then(function (res) {
        console.log(res.data)
    }) 
  </script>

简单请求服务器代码如下:

const express=require('express')
app=express()
app.listen(1002,()=>{
    console.log('启动了')
})
app.use((req,res,next)=>{
    res.header("Access-Control-Allow-Origin","*")
    req.method==='OPTIONS'?res.send('options请求'):next()
})

app.get('/test',(req,res)=>{
    let result={
        code:0,
        data:'hello'
    }
    res.send(result)
})

非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。 浏览器会以 OPTIONS 方法发出一个预检请求,浏览器会在请求头中加入:

Access-Control-Request-Headers:content-type
Access-Control-Request-Method:POST

这个预检请求的作用在这里就是告诉服务器:我会在后面请求中以 POST 方法发送 Request Header 带有 Content-Type 的请求,询问服务器是否允许。 在这里服务器还没有做任何允许这种请求的设置,所以浏览器控制台报错:

image.png 也清楚的说明了出错的原因: 服务端在预检请求的响应中没有告诉浏览器允许协议头 Content-Type,即服务端需要设置响应头 Access-Control-Allow-Headers,允许浏览器发送带 Content-Type 的请求。
服务器中代码如下:

res.header("Access-Control-Allow-Headers", "Content-Type");

可以看到请求成功

image.png
再来看请求的具体信息,第一次以 OPTIONS 方法发送预检请求,浏览器设置请求头:

Access-Control-Request-Headers:content-type
Access-Control-Request-Method:POST

服务端设置响应头:

Access-Control-Allow-Headers: Content-Type

Access-Control-Allow-Origin:http://127.0.0.1:5500

这样预检请求成功了,浏览器会发出第二个请求,这是真正请求数据的请求:

image.png 可以看到 POST 请求成功了,第二次请求头中没有设置 Access-Control-Request-Headers 和 Access-Control-Request-Method。
预检请求的回应 服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,就会触发一个错误。

浏览器正常请求回应

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

还有一种情况我们经常遇到。浏览器在发送请求时需要给服务端发送 cookie,服务端根据 cookie 中的信息做一些身份验证等。 默认情况下,浏览器向不同域的发送 ajax 请求,不会携带发送 cookie 信息。如果想要跨域的ajax请求能够发送凭证信息需要在浏览器和服务器两端分别设置。 浏览器端:

axios.defaults.withCredentials = true

服务器端:

res.header("Access-Control-Allow-Credentials",true)

注意:

  1. axios发送post/put请求时,content-Type的值默认为application/json
  2. 如果服务端允许所有源的话,不安全,也不能携带资源凭证,一般设置为单一源,这样既安全,又可以携带资源凭证 JSONP 和 CORS 比较 JSONP 只能实现 GET 请求,而 CORS 支持所有类型的 HTTP 请求。使用 CORS ,开发者可以是使用普通的 XMLHttpRequest 发起请求和获取数据,比起 JSONP 有更好的错误处理。虽然绝大多数现代的浏览器都已经支持 CORS,但是 CORS 的兼容性比不上 JSONP,一些比较老的浏览器只支持 JSONP。CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。