天天看点

至简·跨域 JSONP与CORS

什么是跨域

什么是跨域呢,对于从未听过的人来说,是一个听起来挺唬人名字。其实也没什么,完全可以通过字面意思去揣摩,坦白讲,就是跨越不同的区域,就这个意思,当然为什么要去不同的区域呢,因为要拿哪里的资源呀,嘿嘿嘿嘿…。

咳咳,正经点啊,用官话来说:跨域,什么是跨域呢?

就是指一个域名下的网站去请求另一个域名下网站的资源。为什么或出现跨域这种事情呢。原因就在于。浏览器的同源策略,就是为了保密隐私,为了自身的安全。并且ajax本身禁止发送跨域请求

跨域分以下几种情况:

  1. http://www.acb.com -------ajax----- >http://uu.abc.com 域名级别不同
  2. http://hr.abc.com -------ajax----- >http://oa.abc.com 域名不同
  3. http://localhost:3000 -------ajax----- > http://localhast:8080 端口不同
  4. http://baidu.com -----ajax-----> https://baidu.com 协议不同

    协议不同的本质还是端口不同,https是443端口,http是80端口

  5. http://localhost ------ajax----> http://127.0.0.1 这个是个规定,规定自己本机下的这种访问方式为跨域。

    以上这几种情况的ajax请求均属于跨域请求。

知道了什么是跨域,那么就该去解决这问题,不然,我们怎么去拿到我们想要的数据呢。提供两种解决方案:

1.JSONP实现跨域

   先说说什么是JSONP,JSON with Padding,填充式JSON,它是将其他网站上的JSON资料,作为参数填充,填充到哪里嫩?,其实使用jsonp时,后端会接受到一个参数,这个参数其实就是前端定义好了的一个处理函数的函数名,这个处理函数,需要一些数据,它将这些数据呢以外部传参的形式呈现出来,,而我们也说了,jsonp请求的时候会将这个函数名获取到,我们在使用一些拼接手法,将参数和函数名结合起来形成一个完整函数名加参数的形式,然后返回给客户端,客户端就可以得到自己所需的参数,又因为数据是以json的格式传输的,就形成了这种填充式json的感觉,这也就是jsonp名字的来由,不得不说,这个方法是真的秒啊。 最后在加上动态添加script标签,数据到了前端后,就会自动调用这个函数。

   至于前端是怎么请求的呢,当然是通过script标签的src啦,为什么要用script标签的src呢,你有没有发现你在引用第三方插件时,不论是网上的url,还是本地的url,script它都能找到啊,有木有??简直比隔壁老王翻墙的本事都厉害啊…有木有。为什么呢?难不成,他真的是跟隔壁老王学得,而且青出于蓝胜于蓝。 不仅如此哦,html标签还有几个也可能是跟老王学的,例如iframe、style、还有img标签,它的src也可以,emmm…这几个不学好。

   开个玩笑哈,为什么呢,这是浏览器给的特权,是浏览器动态的访问外域文件的一种方式,结果被大家给利用了,真的是单纯的孩子。利用script的src这一特点,便可以实现跨域访问。当使用script标签进行外域资源请求时,就会将后端定义好的代码下载到本地执行。

**2018-11-22日 — 补充**

具体过程:

  • 前端通过script标签发起资源请求,参数中携带定义好的函数名。
  • 后端会将获取到的函数名以及所需的参数作为回发数据,响应至客户端,被客户端下载,客户端下载到代码后,将得到的函数及参数添加到新的script标签中,就相当于执行之前定义的函数,只是,函数调用以及传参是由后端发过来的。

相信你已经大致了解了这个过程,兴许你还能发现jsonp的一些缺陷,即它的请求方式类似于get请求,不能进行post请求,如果要想向服务端post数据,会报请求格式不正确错误,并且如果有,那么这种方式的数据都在url中,如果post一堆数据,url也会显得冗余,拼一长串也不太现实。并且如果要保存登录状态,这种方式是无法携带认证信息,jsonp是绕过了ajax请求请求资源的机制,转而使用src的特性获取资源,后端不能获取一致的session,无法进行登录状态的检查,然后就是jsonp的安全性问题,它无法设置请求限制,要是服务器不值得信赖,可能会下载到恶意的执行代码,例如重定向到非法网站盗取用户信息,或者简单点的拿到一个死循环代码,那不就凉凉了。

所谓的padding其实是指返回来的函数及参数,在此作以修正。

都退后,我要开始装B了, 等一等,我想多说几种方案。论一论他们的优缺点。

  • 方案一:

    服务端:将要返回的数据,填充进一条字符串格式的js语句中,组成一条正确的可执行的js语句,在返回到客户端。

    客户端:添加script标签,“<script src=“服务端地址”>”

    结果script标签可以跨域访问,请求到服务端返回的js语句,并立刻执行。

例如服务端,返回的数据可以这样写

const http = require('http');
	const url = require('url');
	http.createServer(function(req,res){
       var params = url.parse(req.url,true);
       console.log(params);//在方案三时有用
       res.writeHead(200,{
          "Content-Type":"application/json;charset=utf-8"
       });
       res.write(`alert("疼")`);
       res.end();
    }).listen(8080,()=>{
    console.log("Jsonp server is running");
});
           

客户端,我未设路由,所以直接写服务端地址即可

<script src="http://127.0.0.1:8080"></script>
           

客户端加载网页时便会弹出警告框。这只是一个简单的实现。我们并不满足于此。

  • 方案二:

    服务端:返回一条自定义函数的调用语句,客户端必须执行指定名称的函数

    客户端:提前定义一个与服务端返回的同名的函数,有一个参数可以接受服务端的数据。

    例如:

    服务端:

var data = "Welcom";
    res.write(`fun("${data}")`);
    res.end();
           

客户端:

<script>
  function fun(data){
        alert(data);
    }
</script>
<script type="text/javascript" src="http://127.0.0.1:8080?fun"></script>
           

函数定义要在返回数据的script标签之前进行,这里的结果也会弹出警示框,并且携带了数据。但是美中不足的是,函数名是写死的,不可能说我们在请求数据前,先跟服务端商量好,函数名用那个,那不搞笑呢么。

  • 方案三

    这里我们可以从前端请求时预先在参数内设定一个callback参数,像这样。

<script type="text/javascript" src="http://127.0.0.1:8080?callback=fun"></script>
           

为什么这样,因为我们在后端接收请求时,可以获取到callback参数,通过对url部分转对象后可以获取

var params = url.parse(req.url,true);
     console.log(params.query.callback);
           

这是获取到的部分

query: { callback: 'fun' },
           

以此便可以使后端不受前端的限制,前端可以随意定义自身所需的处理函数,并且在客户端通过js代码动态的创建script标签,并为其src属性设置好请求的url。服务端也不用在乎前端定义的是什么函数名了。

操作如下:

服务端:

const http = require('http');
const url = require('url');
http.createServer(function(req,res){
    var params = url.parse(req.url,true);
    console.log(params);
    console.log(params.query.callback);
    res.writeHead(200,{
       "Content-Type":"application/json;charset=utf-8"
    })
    var data = "Welcom";
   
    if(params.query.callback){
        var str3 = `${params.query.callback}("${data}")`;//定义要送函数名和函数参数
        console.log(str3);
        res.end(str3);
    }else{
        res.end();
    }
   
    //var data = "Welcom";
    //res.write(`fun("${data}")`);
    //res.end();
}).listen(8080,()=>{
    console.log("Jsonp server is running");
});
           

客户端:

<script src="js/jquery-1.11.3.js"></script>
<script>
	//用jq的方式创建标签,原生js也可以
    $('#btn1').on('click',function(){
        $(`<script type='text/javascript'  src='http://127.0.0.1:8080?callback=fun'>`).appendTo('body');
    });
    
    function fun(data){
        alert(data);
        $('body>script:last-child').remove();//每次请求删除上一次请求的script标签,防止script标签因为请求次数不断的添加。
    }
</script>
<script type="text/javascript" src="http://127.0.0.1:8080?callback=fun"></script>
           

好了,最后一种方案是相对于前两种方案的优化,推荐使用

2. CORS,此方法需要服务端与客户端的配合

CORS跨域资源共享

服务端的响应头设置:

res.writeHead(200,{

“Content-Type”:“application/json;charset=utf-8”,

"Access-Control-Allow-Origin”:"*" | " 指定的来源域名"

})

这样即可, 设置为"*"的方式会不限制任何域名的访问,当然也可以指定允许访问的来源域名,这种方式虽然简单,但并不安全。

最后说一下JQ中实现JSONP跨域的方式,

$.ajax({
	//在1.x版本中
url:" ... ",
type: "get"|"post",
data:"变量=值&..."|{变量:值,...},
dataType:"json",//通过这句将其设为jsonp,便会实现jsonp跨域
success:function(res){ //在响应成功结束后自动
     //res就是响应的结果
 	  },
error(){ 当请求出错时,自动触发 },
complete(){ 无论成功还是出错,只要请求结束就执行 }
	 })
//在 3.x版本中,实现了promise
.then(function(res){
 	 ...
 })
           

使用JQ进行jsonp跨域请求,不需要我们设置函数名,jq会随机生成函数名,也会自动清除每次请求产生的script标签,极大的放便了操作。其原理与方案三是一致的。

OK,完结,撒花

并未结束 **2018/11/22日补充**

之前的跨域设置针对简单跨域,例如只设置了

Access-Control-Allow-Origin: ”*“
           

的跨域,先说说简单跨域和复杂跨域:

简单跨域,即get、post、head请求中的一种,并且头信息也有几项限制,最突出的一点就是Content-Type只能为:application/x-www-form-urlencoded,multipart/form-data和text/plain中的一种

复杂跨域:不是以上两种情况的,

对于复杂跨域,还需要设置一些相关信息。若要进行服务端验证或者携带cookie还需要在客户端和服务端配合设置。

由于跨域请求默认不发送Cookie和HTTP认证信息。相信应该明白,同源策略的目的,就是为了保密,为了防止数据被盗取,cookie中携带着用户很多的信息,所以这好似服务端与客户端两个人之间的小秘密,不能被别人知道了,万一被截取就可能发生跨域伪造请求的问题。但是实际生活中,我们经常会进行在线验证等等,不能所有情况下都要重新登录,这时就要保证服务端的完全信任了,然后携带cookie,如果非要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段:Access-Control-Allow-Credentials: true,另一方面,开发者在发起ajax请求时设置withCredentials为true。

   在这里,当我们的服务器设置Access-Control-Allow-Credentials: true时,会产生新的问题,在浏览器标准中,当服务器中设置Access-Control-Allow-Credentials为true时,Access-Control-Allow-Origin不能设置为*,而Access-Control-Allow-Origin: *是我们常用的解决跨域问题的设置。

此问题的解决方案有两种,第一种方案是简单的设置一个白名单;另一种方案,如果之前设置Access-Control-Allow-Origin: *,此时可以在服务器配置文件进行设置:先获取发起跨域请求的源域,然后设置Access-Control-Allow-Origin的值为获取到的源域。当然这个设置可能在后端某些配置文件里,也可能直接在服务器配置文件设置。但思路一致。

具体实现:

客户端说一下以下几种条件下的使用:

默认跨域不携带cookie,所以需要声明允许携带cookie。

(1) 原生js中设置

xhr.withCredentials=true;
           

(2)jq方式的在配置参数中设置

xhrFields:{
            withCredentials:true
        },
           

(3)在axios中的设置

import axios from 'axios';
//设置允许携带cookie
axios.defaults.withCredentials=true;
           

服务端的配置,仅讨论nodejs环境的配置方式,其他服务器的配置信息也一致

(1)设置响应头,允许跨域携带认证信息:

我这里使用express的框架

res.header(“Access-Control-Allow-Credentials”, true);

(2)在允许跨域携带认证信息后,

Access-Control-Allow-Origin",不能为“*”,需要设置允许的源域名,或者可以自己设置一个白名单,允许这些可以跨域访问。

res.header(“Access-Control-Allow-Origin”, “http://localhost:8080”);

这样就可以实现,携带认证信息的进行访问。

继续阅读