跨域
想要了解跨域,首要要了解源
什么是源,源等于协议加域名加端口号
只有这三个都相同,才是同源,反之则是非同源。
比如下面这四个里,只有第4个是同源
而浏览器给服务器发送请求时,他们的源一样,就是同源请求,反之就是非同源请求,非同源请求又称为跨域。
当你是跨域时,浏览器为了确保资源安全,会对跨域的访问资源做出一些限制,也就是浏览器的同源策略。
这是W3C上对同源策略的说明:https://www.w3.org/Security/wiki/Same_Origin_Policy
浏览器会对跨域做出哪些限制?
例如:源a和源b,它们是非同源的,则浏览器会有如下限制:
1. DOM访问限制:源a的脚本不能读取和操作源b的DOM。
页面1
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <h1>我是页面1</h1> 10. <button onclick="showDOM()">获取页面2的DOM</button> 11. <br> 12. <iframe id="framePage" src="./demo.html"></iframe> 13. <!-- <iframe id="framePage" src="https://www.baidu.com"></iframe> --> 14. 15. <script type="text/javascript" > 16. function showDOM(){ 17. const framePage = document.getElementById('framePage') 18. console.log(framePage.contentWindow.document.body) //同源的可以获取,非同源的无法获取 19. } 20. </script> 21. </body> 22. </html>
页面2
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <h2>我是页面2</h2> 10. </body> 11. </html>
页面是这样的
点击获取
换一个不同源的页面
1. <body> 2. <h1>我是页面1</h1> 3. <button onclick="showDOM()">获取页面2的DOM</button> 4. <br> 5. <!-- <iframe id="framePage" src="./demo.html"></iframe> --> 6. <iframe id="framePage" src="https://www.bilibili.com"></iframe> 7. 8. <script type="text/javascript"> 9. function showDOM() { 10. const framePage = document.getElementById('framePage') 11. console.log(framePage.contentWindow.document.body) //同源的可以获取,非同源的无法获取 12. } 13. </script> 14. </body>
获取DOM
获取不到
2. 第二个限制,源a不能访问源b的cookie
3. <!DOCTYPE html> 4. <html lang="en"> 5. 6. <head> 7. <meta charset="UTF-8"> 8. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9. <title>Document</title> 10. </head> 11. 12. <body> 13. <h1>我是页面1</h1> 14. <button onclick="getCookie()">获取页面2的cookie</button> 15. <br> 16. <!-- <iframe id="framePage" src="./demo.html"></iframe> --> 17. <iframe id="framePage" src="https://www.bilibili.com"></iframe> 18. 19. <script type="text/javascript"> 20. function getCookie() { 21. const framePage = document.getElementById('framePage') 22. console.log(framePage.contentWindow.document.cookie) //同源的可以获取,非同源的无法获取 23. } 24. </script> 25. </body> 26. 27. </html>
还是一样的页面,这次获取的是cookie
也是获取不到的,因为你在获取document这一步就已经失败了
3. ajax响应数据限制:源a可以给源b发请求,但是无法获取源b响应的数据。
这是一个获取头条新闻的页面
代码
28. <!DOCTYPE html> 29. <html lang="en"> 30. <head> 31. <meta charset="UTF-8"> 32. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 33. <title>Document</title> 34. </head> 35. <body> 36. <button onclick="getNews()">获取头条新闻</button> 37. 38. <script> 39. async function getNews() { 40. const result = await fetch('https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc'); 41. const data = await result.json(); 42. console.log('data'); 43. } 44. </script> 45. </body> 46. </html>
页面
获取不到
备注:在上述限制中,浏览器对 Ajax 获取数据的限制是影响最大的一个,且实际开发中经常遇到。
几个注意点
- 跨域限制仅存在浏览器端,服务端不存在跨域。
- 即使跨域了,ajax请求也可以正常发出,但响应数据不会交给开发者。
- link,script,img这些标签发出的请求也可能跨域,只不过浏览器对标签跨域不做严格限制,对开发几乎无影响。
那么要如何解决跨域
想要解决跨域,一般有三种方法
第一种,用CORS解决ajax跨域问题
CORS 概述
CORS 全称:Cross-Origin Resource Sharing(跨域资源共享),是用于控制浏览器校验跨域请求的一套规范,服务器依照 CORS 规范,添加特定响应头来控制浏览器校验,大致规则如下:
- 服务器明确表示拒绝跨域请求,或没有表示,则浏览器校验不通过。
- 服务器明确表示允许跨域请求,则浏览器校验通过。
了解了CORS之后,还要了解一下简单请求和复杂请求
CORS 会把请求分为两类,分别是:1.简单请求、2.复杂请求。
简单请求要满足这个三个条件
- 请求方法要为 get, head或者post。
- 请求头字段要符合CORS安全规范(https://fetch.spec.whatwg.org/#cors-safelisted-request-header), 一般只要不手动修改请求头,都能符合改规范。
- 请求头的Content-Type的值只能是这三种:
text/plain
multipart/form-data
application/x-www-form-urlencoded
不满足这三个要求的请求都是复杂请求,而复杂请求会自动发送预检请求。
预检请求是一种在实际跨域请求发出之前,由浏览器自动发出的一种请求。主要用于向服务器确认是否允许接下来的跨域请求。基本流程是这样的,浏览器先发起一个options请求,携带这个几个请求头,Origin(发起请求的源),Access-Control-Request-Method(实际请求的HTTP方法),Access-Control-Request-Headers(实际请求中使用的自定义头)。如果通过预检,才会继续发起实际的跨域请求。
这就是预检请求
CORS 解决简单请求跨域
前端
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <button onclick="getData()">获取数据</button> 10. 11. <script> 12. async function getData() { 13. const url = 'http://127.0.0.1:8081/'; 14. const result = await fetch(url); 15. const data = await result.json(); 16. console.log('data'); 17. } 18. </script> 19. </body> 20. </html>
服务端代码(以express框架为例):
1. const express = require('express'); 2. const app = express(); 3. 4. app.get('/', (req, res) => { 5. res.send([{ name: '张三', age: 19 }]) 6. }) 7. 8. app.listen(8081, () => { 9. console.log('服务器成功启动'); 10. })
这个时候是获取不到数据的
设置下cors
1. const express = require('express'); 2. const app = express(); 3. 4. app.get('/', (req, res) => { 5. res.setHeader('Access-Control-Allow-Origin','http://127.0.0.1:5500') 6. res.send([{ name: '张三', age: 19 }]) 7. }) 8. 9. app.listen(8081, () => { 10. console.log('服务器成功启动'); 11. })
这样就能获取到数据了
整体思路:服务器在给出响应时,通过添加Access-Control-Allow-Origin响应头,来明确表达允许某个源发起跨域请求,随后浏览器在校验时,直接通过。
像这样的设置是允许某个源跨域
1. // 允许 http://127.0.0.1:5500 这个源发起跨域请求 2. res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
而这样则是允许所有源跨域
1. // 允许所有源发起跨域请求 2. res.setHeader('Access-Control-Allow-Origin', '*')
CORS 解决复杂请求跨域
第一步:服务器先通过浏览器的预检请求,服务器需要返回如下响应头
Access-Control-Allow-Origin(允许的源)
Access-Control-Allow-Methods(允许的方法)
Access-Control-Allow-Headers(允许的自定义头)
Access-Control-Max-Age(预检请求的结果缓存时间(可选))
第二步:处理实际的跨域请求(与处理简单请求跨域的方式相同)
服务器代码
1. const express = require('express'); 2. const app = express(); 3. 4. app.options('/', (req, res) => { 5. res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') 6. res.setHeader('Access-Control-Allow-Methods', 'GET') 7. res.setHeader('Access-Control-Allow-Headers', 'city') 8. res.send() 9. }) 10. 11. app.get('/', (req, res) => { 12. res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') 13. res.send([{ name: '张三', age: 19 }]) 14. }) 15. 16. app.listen(8081, () => { 17. console.log('服务器成功启动'); 18. })
前端代码
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <button onclick="getData()">获取数据</button> 10. 11. <script> 12. async function getData() { 13. const url = 'http://127.0.0.1:8081/'; 14. const result = await fetch(url, { method: 'GET', headers: { city: 'beijing' } }); 15. const data = await result.json(); 16. console.log('data', data); 17. } 18. </script> 19. </body> 20. </html>
数据获取到了
预检请求返回的响应头
借助 cors 库快速完成配置
上述的配置中需要自己配置响应头,比较麻烦,借助cors库,可以更方便完成配置
安装cors
npm i cors
配置cors
1. const express = require('express'); 2. const cors = require('cors'); 3. const app = express(); 4. 5. // 使用cors中间件 6. app.use(cors({ 7. origin: 'http://127.0.0.1:5500', // 允许的源 8. methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'], // 允许的方法 9. allowedHeaders: ['city'], // 允许的自定义头 10. exposedHeaders: ['abc'], // 要暴露的响应头 11. optionsSuccessStatus: 200 // 预检请求成功的状态码 12. })) 13. 14. app.get('/', (req, res) => { 15. res.setHeader('abc', '123') 16. res.send([{ name: '张三', age: 19 }]) 17. }) 18. 19. app.listen(8081, () => { 20. console.log('服务器成功启动'); 21. }) 22.
成功获取数据
第二种,JSONP 解决跨域问题
JSONP 概述: JSONP 是利用了<script>标签可以跨域加载脚本,且不受严格限制的特性,可以说是程序员智慧的结晶,早期一些浏览器不支持 CORS 的时,可以靠 JSONP 解决跨域(现在基本不用了)。
基本流程:
- 第一步:客户端创建一个<script>标签,并将其src属性设置为包含跨域请求的 URL,同时准备一个回调函数,这个回调函数用于处理返回的数据。
- 第二步:服务端接收到请求后,将数据封装在回调函数中并返回。
- 第三步:客户端的回调函数被调用,数据以参数的形势传入回调函数。
示例代码
前端
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <script> 10. function callback(data) { 11. console.log('data', data) 12. } 13. </script> 14. <script src="http://127.0.0.1:8081/users"></script> 15. </body> 16. </html>
后端
1. const express = require('express'); 2. const app = express(); 3. 4. app.get('/users', (req, res) => { 5. res.send(`callback([{ name: '张三', age: 19 }])`) 6. }) 7. 8. app.listen(8081, () => { 9. console.log('服务器成功启动'); 10. })
结果
优化下代码
前端
1. <!DOCTYPE html> 2. <html lang="en"> 3. 4. <head> 5. <meta charset="UTF-8"> 6. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7. <title>Document</title> 8. </head> 9. 10. <body> 11. <button onclick="getUsers()">获取用户</button> 12. 13. <script> 14. function test(data) { 15. console.log('data', data) 16. } 17. function getUsers() { 18. // 创建script元素 19. const script = document.createElement('script') 20. // 指定script的src属性 21. script.src = 'http://127.0.0.1:8081/users?callback=test' 22. // 将script元素添加到body中触发脚本加载 23. document.body.appendChild(script) 24. // script标签加载完毕后移除该标签 25. script.onload = () => { 26. script.remove() 27. } 28. } 29. </script> 30. </body> 31. 32. </html>
后端
1. const express = require('express'); 2. const app = express(); 3. 4. const users = [ 5. { name: '张三', age: 19 }, 6. { name: '李四', age: 20 } 7. ] 8. 9. app.get('/users', (req, res) => { 10. const { callback } = req.query 11. res.send(`${callback}(${JSON.stringify(users)})`) 12. }) 13. 14. app.listen(8081, () => { 15. console.log('服务器成功启动'); 16. })
效果
用jQuery封装jsonp
1. $.getJSON('http://127.0.0.1:8081/teachers?callback=?', (data) => { 2. console.log(data) 3. })
第三种,配置代理服务器解决跨域
这里需要用到http-proxy-middleware这个包来配置代理服务器
下载http-proxy-middleware
npm i http-proxy-middleware
前端
1. <!DOCTYPE html> 2. <html lang="en"> 3. <head> 4. <meta charset="UTF-8"> 5. <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6. <title>Document</title> 7. </head> 8. <body> 9. <button onclick="getNews()">获取头条数据</button> 10. 11. <script> 12. async function getNews() { 13. const url = 'http://127.0.0.1:8081/api/hot-event/hot-board/?origin=toutiao_pc'; 14. const result = await fetch(url); 15. const data = result.json(); 16. console.log('data', data); 17. } 18. </script> 19. </body> 20. </html>
后端
1. const express = require('express'); 2. const app = express(); 3. const { createProxyMiddleware } = require('http-proxy-middleware'); 4. 5. app.use(express.static('./public')) 6. 7. app.use('/api', createProxyMiddleware({ // 拦截所有带有/api的请求 8. target:'https://www.toutiao.com', // 转发的目标 9. changeOrigin:true, // 允许跨域 10. pathRewrite:{ 11. '^/api':'' // 把路径中的/api去掉 12. } 13. })) 14. 15. app.listen(8081, () => { 16. console.log('服务器成功启动'); 17. }) 18.
需要把前端代码当成静态资源部署在服务器上
目录
把服务器启动了,需要去http://127.0.0.1:8081获取页面
点击获取数据
数据成功获取