前言
由于浏览器的同源策略,当我们请求网络资源时,所在页面的url中的协议,端口,域名其中一个与请求资源的url不同,都会出现跨域的问题。但是浏览器不能没有这个策略,这样会很危险,像csrf,xss攻击等**。那么这里有个容易理解错误的地方,跨域并不是说服务器没法返回资源给浏览器,而是浏览器没办法正确拿到,这不是服务器的问题。**但是也不是所有的请求都是这样的,像表单提交就不存在什么跨域问题,因为表单不需要服务器返回数据给它,它只负责提交就好了。

几种解决跨域问题的方法
jsonp
jsonp主要是利用了script标签的src属性不受同源策略的影响,通过后端的配合从而解决跨域问题
下面举个栗子:
我们在页面加载完毕后就发起get请求,请求的url是本机的8080端口

 axios.get('http://127.0.0.1:8080')

打开控制台发现报了跨域的错误(这里说明一下,5500端口是vscode的一个插件搭建的服务器)
在这里插入图片描述

然后我们利用jsonp来让浏览器可以正常的接收到服务器返回的数据,jsonp是需要后端配合使用的,先来看代码,后面再仔细捋一遍
前端代码:
这里创建了一个script标签,然后将它的src属性赋值为请求资源的url地址,并且携带query参数过去,这里的query参数callback=handle中的handle在前端是一个函数,随后将script放入页面,一旦放入页面,scr就会去请求资源了

function handle (data) {
            console.log(data)
        }

const script = document.createElement('script')
script.src = 'http://127.0.0.1:8080/?callback=handle'
document.body.insertBefore(script, document.body.firstChild)

后端代码:
后端这里解析了query参数,将callback的值单独拿出来,然后通过这个值来返回数据**(这里由于懒就直接用了querystring,建议还是使用URLSearchParams,因为vscode提醒我querystring废弃了)**

const http = require('http')
const url = require('url')
const querystring = require('querystring')
const server = http.createServer()
server.on('request', (req, res) => {
    const { query } = url.parse(req.url)
    const { callback } = querystring.parse(query)
    res.end(`${callback}('hello')`)
})
server.listen(8080, () => {
    console.log('8080端口监听中...')
})

结果:

在这里插入图片描述

我们再来捋一遍,首先src 不会受到同源策略的影响,所有利用script标签去请求不同源的资源不会报错,那么需要拿到服务器的数据,该怎么办?咱就这么想,服务器返回的数据是需要被解析的,那么就让服务器返回数据时调用一个函数,这个函数的形参就是服务器返回的数据(这个需要服务器配合的),所以我们指定一个query参数过去,让服务器去解析出需要调用的函数,并且给这个函数传参,比如上面后端返回的数据就是handle(‘hello’),那么浏览器收到以后一解析就去运行这个函数了
jsonp虽然可以解决跨域的问题,但是只针对get请求,没见过src是post请求的吧

CORS
我们上面报错的那个截图其实也提到了一个Access-Control-Allow-Origin 这个东西,这个东西是在后端配的,翻译一下就是允许跨域的源,这样一翻译就很明白了吧,就是在后端设置一个这个东西,表示哪些源是可以允许跨域的,如果还是不能理解也没关系,举个栗子嘛:
前端代码:请求本机8080端口资源

const promise = axios.get('http://127.0.0.1:8080')
promise.then((msg) => {
    console.log(msg.data)
}).catch((err) => {
    console.log(err)
})

后端代码:设置所有的源都可以跨域

const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*')
    res.end(`hello`)
})
server.listen(8080, () => {
    console.log('8080端口监听中...')
})

结果:

在这里插入图片描述

这里可能有些小伙伴不知道什么是源,你可以打开控制台,点击network,然后点击你请求到的资源,就可以看见如下信息:
在这里插入图片描述

看见请求标头里面有个Origin了吗,那个就是源,也就是说浏览器本来拿不到这个资源的,但是乍一看,这个源可以共享资源,于是就放行了。当然最好不要设置通配符,还是和前后端一起配合协商比较好,比如我们这个简单例子就可以把通配符改成5500这个源。
当然,不止这么简单设置一下就好了。
cors请求分为简单请求,和非简单请求,只要同时满足以下条件,就属于简单请求。

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

HEAD
GET
POST
HTTP的头信息不超出以下几种字段:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问

请求中没有使用 ReadableStream 对象

简单请求

如果是简单请求的话,Access-Control-Allow-Origin是不可省略的,否则请求按失败处理,除此之外还有Access-Control-Allow-Credentials表示请求中是否包含cookie信息,以及Access-Control-Expose-Headers,它表示XMLHttpRequest对象的getResponseHeader()方法可以拿到的额外字段(默认只能拿到六个字段)

预检请求

凡是不同时满足上面条件,就属于非简单请求。
当请求存在跨域资源共享(CORS)并且是非简单请求,就会触发CORS的预检请求,预检请求用的请求方法是OPTIONS。
如果后端采用token检验机制,前端发送请求必须将token放到请求头中,那么就需要传输自定义Header信息、则请求头中的Content-Type=“application/json”,就会形成非简单请求。
下面我们来简单演示一下复杂请求:
前端代码:把请求改为put请求

const promise = axios.put('http://127.0.0.1:5000')
promise.then((msg) => {
    console.log(msg.data)
}).catch((err) => {
    console.log(err)
})

后端代码:为了可以利用钩子,所以用express,就是说在进入app.put都要先执行app.use,随后才可以放行

const express = require("express");
const app = express();
// 实现CORS
app.use(function (req, res, next) {
    //允许哪个源可以共享资源
    res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
    // 允许哪个方法访问服务器
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    next();
});
app.put("/", (req, res) => {
    res.send('hello put method')
})

app.listen(5000, () => {
    console.log("5000")
})

随后打开控制台,可以发现出现了两次请求,一次是put请求,一次是option请求,仔细观察也可以看见浏览器标识了option请求为预检,为什么发送了一次option请求,就是因为这是一次复杂请求,所以触发了option请求,但是我写的代码并没有对option请求做出响应处理,最好还是做一下处理。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这里的后端代码其实可以写的更严谨一点,不局限于这几个字段,还有一些允许携带cookie什么什么的请求头,也可以根据实际需求去加,所以说后端是cors通信的关键

代理服务器
原理
在这里插入图片描述

跨域的问题根本原因就是返回数据的服务器和请求数据的页面不是一个源,那么就申请一个代理服务器,这个代理服务器和页面在同一个源,所以不会出现跨域的问题,那么这个代理服务器上没有我们需要的数据,所以就把这个请求再转发给有这个数据的服务器上,由于服务器和服务器之间通信不会出现跨域的问题,因为同源策略是浏览器上的,和服务器没关系,所以最后就可以成功把数据请求返回给浏览器。
举个栗子:
假设有个5000端口服务器:在这个服务器上有个login接口,这个接口返回了一些json数据,现在我们有个运行在3000端口的页面需要这些json数据,直接请求会出现跨域的问题,所以我们可以先去请求3000端口,把3000端口当作一个转接器,从而得到数据
这里的5000端口的express是我自己简单封装的一个类,不是express框架,所以写法有点不一样

//5000端口服务器
const express = require('./express')

const app = new express()
app.use('/login', (req, res) => {
    res.end(`{name:'renchel',age:19}`)//需要的数据
})
app.listen(5000, (err) => {
    if (err) {
        return
    }
    console.log("服务器已经启动,端口5000")
})

这里的3000端口是express框架,问我为啥5000不也用express框架写,那就是懒得写,因为5000端口是之前写的,我直接拿来用了

//3000端口服务器
const express = require('express')
const fs = require('fs')
const axios = require('axios')

const app = express()

app.get('/', (req, res) => {
    console.log('/')
    fs.readFile('./index.html', (err, data) => {
        if (!err) res.end(data)
    })
})
app.get('/login', (req, res) => {
    let data;
    (async () => {
        data = await axios.get('http://127.0.0.1:5000/login')
        res.end(data.data)//data.data是从5000端口请求回来的数据
    })()

})

app.listen(3000, (err) => {
    if (err) {
        return
    }
    console.log("服务器已经启动,端口3000")
})
//运行在3000端口的页面
<body>
    <button>button</button>
    <script>
        const btn = document.querySelector('button')
        btn.addEventListener('click', async () => {
            const data = await axios.get('http://127.0.0.1:3000/login')
            console.log(data)
        })
    </script>
</body>

现在捋一遍代码,有个运行在3000端口的页面有个按钮,点击按钮向3000端口的login接口请求数据,3000端口的login接口没有数据于是把这个请求转发给了5000端口的服务器,最后返回回来的数据再通过3000端口返回给页面
效果:
在这里插入图片描述

可以看见数据成功请求回来了。

总结
CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
JSONP只支持get请求,而且无法知晓请求的数据是否成功,如果一直卡在请求中,我们也不知道。
日常工作中,用得比较多的跨域方案是cors和Proxy代理服务器,Proxy主要就是利用同源策略对服务器不起作用。

源码附件已经打包好上传到百度云了,大家自行下载即可~

链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~

开源地址:http://github.crmeb.net/u/lsq
————————————————
版权声明:本文为CSDN博主「仙凌阁」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39221436/article/details/126123030

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐