在同源策略面前,如何进行跨域? —— 三种基础方法

1,164 阅读8分钟

我报名参加金石计划一期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

想必大家都知道,前端的数据一般都是来源于后端,后端对数据做好一系列处理之后,前端就可以对后端发起接口请求,请求数据。

可是,当我们发送接口请求的时候,浏览器会有这么一个策略 “同源策略” 将其拦截,导致前端无法拿到想要的数据,那么我们该怎么应对同源策略呢?

同源策略

那么什么是同源策略呢?

同源策略:即前端的协议号域名端口号与后端的协议号域名端口号都一致符合请求,浏览器才不会对其进行拦截,这就是同源策略

什么是协议号?什么是域名?什么又是端口号?

这里咱们拿百度的链接来说:

这是一个百度的链接:https: // www.baidu.com :8080 /userInfo

https就是协议号

www.baidu.com就是域名

:8000就是端口号(一般情况下端口号都是隐式的,所以我们看不见)。

而后面的/userInfo等等都是路由路径啦

跨域拦截发生在什么时候呢?

前端向后端发送接口请求数据大致可以分为两个阶段,第一阶段是前端把请求发给后端,第二阶段是后端根据前端的请求将数据返回给前端。

那么问题来了,浏览器跨域拦截是发生在第一阶段前端发送接口请求给后端的时候,还是发生在第二阶段后端返还数据给前端呢?

这里咱们给大家做一个例子,大家就能很快理解啦。

咱们创建两个文件夹,分别为client,server

然后打开server文件夹的终端,输入以下命令行(前提是你有安装配置好npm哈):

npm init -y                      //初始化文件夹
npm i koa                        //安装koa

那么,(如果你下了node.js)之后咱们就可以在server里面引入koa然后运行后端啦;

咱们在server文件夹里创建一个app.js文件,然后写下以下代码:

const Koa = require('koa')
const app = new Koa();

const main = (ctx,next) =>{
    console.log(ctx.query.name)
    ctx.body = 'hello world'
}

app.use(main)

app.listen(3000,()=>{
    console.log('项目已启动!')
})

然后在server文件夹下终端输入node app.js命令行运行后端代码,运行成功就有如下效果:

1662473398390.png

然后去浏览器的url输入localhost:3000回车,那么咱们就能看到后端的运行啦。

1662473497539.png

后端这边弄完了,接下来咱们就该去前端,对后端发送接口请求了,打开client文件夹,然后创建一个index.html文件,引入jquery的js;然后用$.ajax发起接口请求,于是有了以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>    //引入jquery
    <title>Document</title>
</head>
<body>
    <button id="btn">获取数据</button>   //点击发起接口请求
    <script>
        let btn = document.getElementById('btn')
        btn.addEventListener('click',()=>{
            $.ajax({                              //jquery接口请求
                url:'http://localhost:3000',
                data:{name:'江湖'},              //传给后端的数据
                method:'get',
                success(res){
                    console.log(res)         //如果接口请求成功,那么回调会打印res
                }
            })
        })
    </script>
</body>
</html>

然后咱们去运行这个前端,点击按钮,发起接口:

1662473837506.png

然后我们打开页面检查,可以发现报错了:

1662473939555.png

记住这个报错,这就是跨域被拦截然后才会报的错,然后咱们再回到后端的终端这边:

1662474119357.png

可以看到后端这里咱们打印出来了前端传给后端的数据,说明前端发给后端的请求已经被后端接收到了,可是前端仍然没有拿到数据,报错也显示被跨域拦截了,说明后端返还数据给前端的时候,数据被跨域拦截下来了。

那么咱们可以得出结论:跨域拦截发生在后端拿到前端接口请求后返还数据给前端的时候。

怎么解决跨域拦截呢?

解决跨域拦截的方法有很多,但是大多方法都是有一定的弊端的,这里咱们主要讲一下三种比较基础的方法;

这三种方法分别是:

1. JSONP

2. Cors(cross-Origin Resouce Sharing)(后端开启)

3. node代理 (vue.config.js)

JSONP

什么是JSONP呢?

讲到JSONP那么咱们先讲到html里的一些标签吧,不知道大家有没有想起来html里面有几个标签也是可以向其它地方请求数据,而且还不会被跨域拦截奥。

它们就是:

link标签:link标签有一个src属性,link标签可以直接向src属性里的链接拉取数据,可是它却不会被跨域拦截。

img标签:同上,具有src属性。

script标签:同上,具有src属性。

JSONP方案就是利用这些标签属性来进行请求数据,从而做到不被跨域拦截。

那么咱们就接上之前的用例啦,咱们将client文件夹下的index.html里面script标签里面的代码替换成如下(其它部分一切不变):

<script>
        const josnp = (url,params,cb) =>{
            return new Promise((resolve,reject)=>{
                const script = document.createElement('script');
                params = {...params,cb:cb}
                const arr = Object.keys(params).map(key => `${key}=${params[key]}`)
                script.src = `${url}?${arr.join('&')}`
                document.body.appendChild(script)
                //后端会返回一个函数给前端
                window[cb] = (data)=>{
                    resolve(data)
                }
            })
        }
        let btn = document.getElementById('btn')
        btn.addEventListener('click',()=>{
            josnp('http://localhost:3000',{name:'江湖',age:18},'callback').then(res=>{
                console.log(res)
            })
        })
</script>

定义一个josnp函数,在下面的btn.addEventListener的点击事件里面进行监听,一旦点击button按钮,则调用josnp函数,并向其中传参,第一个参数'http://localhost:3000'是我们要去的后端地址,

第二个参数{name:'江湖',age:18}是前端向后端传的数据;

第三个参数'callback'是一个字符串,让后端将数据放入这个字符串后面,让它传回前端后成为一个函数调用指令。

再回到josnp函数里面,

const script = document.createElement('script');是创建一个script标签;

params = {...params,cb:cb}是将我们要传给后端的数据解构放入一个对象里面,同时把我们要从后端拿到的数据也放入里面;

const arr = Object.keys(params).map(key => key={key}={params[key]})则是将这个对象里面的数据转化成一个'key=value'字符串的数组的形式

script.src = ${url}?${arr.join('&')}则是将我们要向后端发的链接地址放入src属性中,发起请求。

document.body.appendChild(script)是将srcipt标签放入html标签中,让它生效(img标签比较特殊,不需要放入html标签里也是可以生效的)。

window[cb] = (data)=>{ resolve(data) }这是重点,后端传回来的数据应该是这个callback('后端给前端的数据'),是一个调用指令,那么就会调用一个名字叫callback的函数,那么这个函数在哪呢?

它就是window[cb]了,cb是我们传参的callback,是在window下生效的,可以被返还过来的指令直接调用到的。

前端代码解读就这样的,那么前端是要求后端返还给咱们一个指令,咱们就可以去后端做一些操作啦。

server文件夹下app.js代码修改如下:

const Koa = require('koa')
const app = new Koa();

const main = (ctx,next) =>{
    console.log(ctx.query)
    const {name,age,cb} = ctx.query
    const userInfo = `${name}今年${age}岁`
    const str = `${cb}(${JSON.stringify(userInfo)})`
    ctx.body = str
}

app.use(main)

app.listen(3000,()=>{
    console.log('项目已启动!')
})

这里无非是对前端给我们的数据做了一些处理,然后返还给前端一个callback(userInfo)的调用指令;

那么咱们再次运行前后端,结果是这样的:

前端打印:

1662477699199.png

后端打印:

1662477711383.png

可以看到的是,前端拿到了后端传过来的数据,后端也拿到了前端给后端传的数据,跨域拦截被我们避开了。

这就是JSONP方法避开跨域拦截,利用html特殊属性的标签来达到避开跨域拦截,但是需要注意的是,利用src就注定发送的请求类型是get请求,那么我们传给后端的数据将会被加在链接后面的路径显示出来,从而达不到数据保密的效果,这就是它的弊端。

Cors(cross-Origin Resouce Sharing)(后端开启)

Cors方案咱们来讲两种写法,一种比较简单,只需要引入一些东西即可,一种则是咱们用原生JS去实现同样的效果。

而Cors方法避开跨域拦截的原因则是让后端对前端的协议号域名端口号放开,放入白名单,让前端请求数据不再会被跨域拦截,当然,这个方法的弊端是只适用于开发环境,一旦发布上线了,放开的白名单就会导致他人可以直接请求服务器数据,这就是它的弊端。

Cors方法一

首先我们在后端已安装koa文件的基础上,再调用指令行npm install @koa/cors --save安装一个cors,于是我们就可以在后端加上这两行代码:

const cors = require('@koa/cors');

app.use(cors());            //后端开启cors,允许跨域操作

那么后端代码就变成了这样:

const Koa = require('koa')
const app = new Koa();
const cors = require('@koa/cors');

app.use(cors());            //后端开启cors,允许跨域操作

const main = (ctx,next) =>{
    console.log(ctx.query.name)
    ctx.body = 'hello world'
}

app.use(main)

app.listen(3000,()=>{
    console.log('项目已启动!')
})

前端代码不作任何改变,依旧如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
    <title>Document</title>
</head>
<body>
    <button id="btn">获取数据</button>
    <script>
        let btn = document.getElementById('btn')
        btn.addEventListener('click',()=>{
            $.ajax({
                url:'http://localhost:3000',
                data:{name:'江湖'},
                method:'get',
                success(res){
                    console.log(res)
                }
            })
        })
    </script>
</body>
</html>

只需要这样引入一个cors就可以让后端放开对前端请求的白名单了,那么前端就可以发送请求到后端并拿到数据啦。

是不是特别简单,只需要作为前端的你能够压迫后端安装一个cros,以及让他多加两行代码,haha。

cros方法二

cros第二个方法跟第一个cros方法是一样的,只不过第一个方法咱们采用的是直接外部安装引入的,而第二个方法则是咱们自己去手写这部分封装的代码,从而达到与第一个cros相同的效果。

那么我们将后端代码全部注释或者删除,然后重新启动web请求,前端代码仍旧不变,后端代码如下:

const http = require('http')

const server = http.createServer((req,res)=>{    //调用http.createServer启动服务

    //开启cors,后端在响应头中设置
    res.writeHead(200,{
        "Access-Control-Allow-Origin":"*",     //协议白名单,*为所有都可以访问
        "Access-Control-Allow-Methods":"GET,POST,PUT,OPTIONS",
        "Access-Control-Allow-Headers":"Content-Type"  //允许前端设定请求头的类型,不管向浏览器返回什么类型都可以
    })

    res.end('hello cors')               //向前端输出返回的东西
})

server.listen(3000,()=>{            //监听电脑的一个端口
    console.log('项目已启动!')
})

那么,我们这里的后端的代码主要是在响应头中设置了一些操作,那么什么是响应头呢?

来,我们随便打开一个网页进行检查,这里咱们打开一个百度,然后去到Network(网络)里检查,你会发现,一个请求会有三部分,请求头请求体响应头

1662514897464.png

而我们之前也说过了,前端发请求给后端,从前端到后端这个过程中,我们可以理解为请求头在发挥作用,我们对于向后端的数据要求都可以在请求头中操作好。

请求体咱们就暂且不说,而响应头呢,则是后端接收到前端的请求后,返还给前端相应的数据,而在返还前端的过程中做操作的地方啦,这里我们就是在响应头中操作让后端放开白名单,然后使浏览器不再会跨域拦截后端返还给前端的数据。

用这种操作我们仍旧能在前端拿到请求的数据。

注意:这种操作只适用于开发环境下。

node代理 (vue.config.js)

node代理其实是一种在利用了跨域拦截的特性来解决跨域的一种方法。

我们都知道,跨域是发生在后端向前端返还数据的时候,那么,拦截发生在哪里?发生在浏览器,这是浏览器对于后端返还给前端的数据拦截,所以是发生在浏览器里。

那么,简单来说,是不是只要我们的数据不经过浏览器,那么是不是就可以不被拦截呢?

事实确实如此,但是怎么做到请求数据不经过浏览器呢?

这里咱们就采用一个投机取巧的方式,在自己的服务器写一个后端向别人的后端请求数据,这个样子是不会经过浏览器的,因为只有后端返还数据给前端才会经过浏览器,而一个后端向另外一个后端请求数据,再由另外一个后端返还数据回来给这个后端,就不会经过浏览器了。

图解就是这样的:

image.png 1662516839804.png

代码操作如下:

别人的后端:

//别人的后端
const Koa = require('koa')
const app = new Koa();

const main = (ctx,next) =>{
    console.log(ctx.query.name)
    ctx.body = 'hello world'
}

app.use(main)

app.listen(3000,()=>{
    console.log('项目已启动!')
})

咱们的后端:

//自己的后端

const http = require('http')

const server = http.createServer((req,res)=>{

    //开启cors,后端在响应头中设置
    res.writeHead(200,{
        "Access-Control-Allow-Origin":"*",     //协议白名单,*为所有都可以访问
        "Access-Control-Allow-Methods":"GET,POST,PUT,OPTIONS",
        "Access-Control-Allow-Headers":"Content-Type"  //允许前端设定请求头的类型,不管向浏览器返回什么类型都可以
    })
    
    //向别人的后端请求数据
    http.request({
        host:'127.0.0.1',
        port:'3000',
        path:'/',
        method:'GET'
    },proxyRes=>{
        // console.log(proxyRes)
        proxyRes.on('data',result=>{
            // console.log(result.toString())    //result是一个buffer流字符,
            res.end(result.toString())           //toString()将buffer流字符转化成正常字符,返还给前端
        })
    }).end()
})

server.listen(3001,()=>{
    console.log('my项目已启动!')
})

咱们的前端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
    <title>Document</title>
</head>
<body>
    <button id="btn">获取数据</button>
    <script>
        let btn = document.getElementById('btn')
        btn.addEventListener('click',()=>{
            $.ajax({
                url:'http://localhost:3001',
                data:{name:'江湖'},

                //为了告诉后端,你返回的响应头类型应该是xxx
                headers:{Accept:"application/json;charset=utf-8"},
                method:'get',
                success(res){
                    console.log(res)
                }
            })
        })
    </script>
</body>
</html>

这种node代理的方法就是自己造一个后端作为数据的中转站,然后在自己的后端进行取消对前端的跨域拦截,设置白名单。(同样只是适用于开发环境)