前端跨域主要分为一下几个知识点:
跨域问题的产生和意义
JSONP跨域的解决方案和底层原理
CORS(Cross-Origin Resource Sharing)跨域资源共享
基于HTTP Proxy实现跨域请求
基于postMessage实现跨域处理
基于Websocket实现跨域的解决方案
基于Nginx反向代理实现跨域处理
同源策略是Netscape公司于1995年引入浏览器的,它最开始的目的是为了隔离不同网站的数据。由于现在的网站都广泛的依赖于HTTP Cookies来维持自身的状态,那么暴露Cookies就显得十分危险了,这时候就需要有一种约束机制,让客户端严格隔离无关站点提供的内容,防止机密数据丢失。
同源策略(SOP, Same-Origin-Policy)即前端可以访问当前URL下的任何服务器,但只有协议号、域名、端口三者有任何一项不同,那么就不能构成同源,此时访问服务器就是跨域请求。跨域是一种浏览器禁止的行为,而不是后端服务器禁止的行为,所以我们在后端HTTP报文流出时,添加浏览器认可的请求头,让浏览器允许通过,来达到跨域的目的
一般是通过设置CORS Header的内容实现跨域,常见的CORS Header主要有一下几种:
Access-Control-Allow-Origin : 指示请求的资源能共享给哪些域,可以是具体的域名或者*表示所有域。
Access-Control-Allow-Credentials : 指示当请求的凭证标记为 true 时,是否响应该请求。
Access-Control-Allow-Headers : 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
Access-Control-Allow-Methods: 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。
跨域请求报错:
1 Access to XMLHttpRequest at 'http://localhost:1000/books' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
XMLHttpRequest 可以在页面不刷新的情况下请求服务器资源,是实现Ajax技术的原理
JSONP 在前端代码中,凡是https://ymlog.cn/file/script/link/iframe
标签,可以请求任意服务器的数据,不会造成跨域问题,JSONP就是利用script
标签的这种特性,客户端请求时将Callback
函数放在src
中,服务端将函数名和数据部分作为响应一起发送给客户端,客户端收到后直接执行函数名并将数据作为参数传入,从而绕过跨域的限制。
JSONP的几个缺点:
func为全局函数
JSONP需要服务器端支持
JSONP只能处理GET请求
响应能被拦截,不安全
后端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const express = require ("express" );const app = express();const http = require ("http" ).createServer(app);const PORT = 1000 ;const cors = require ("cors" );app.use(express.static('src' )); app.get("/books" , (req, res ) => { let { callback = Function .prototype } = req.query; let data = { one: '沉默的大多数' , two: '尘埃落定' } res.send(`${callback} (${JSON .stringify(data)} )` ); }) http.listen(PORT, () => { console .log(`listen on http://localhost:${PORT} ` ); })
前端代码(jquery实现):
1 2 3 4 5 6 7 8 $.ajax({ url: 'http://localhost:1000/books' , dataType: "jsonp" , method: 'get' , success: data => { console .log(data); } })
可以看到,jquery帮我们构造了一条 这样的请求:
1 http://localhost:1000/books?callback=jQuery35103885115371400778_1604278755622&_=1604278755623
&_
后面的内容是为了防止浏览器缓存添加的字段,可以不用考虑,jQuery后面一串数字是jquery帮我们生成的函数名, 我们发送的请求就是:
1 http://localhost:1000/books?callback=funcName
后端接收到请求,将callback赋值为funcName,然后把funcName(data)这样的字符串发送给客户端,客户端拿到之后直接执行,执行的就是客户端自己定义的funcName,这里是success函数执行结果。这是用jquery封装的JSONP,原生的JSONP应该是如下内容(后端代码不需要修改):
1 2 3 4 5 6 7 8 9 10 请求 <script> function getBooks (data ) { console .log("接收到了数据:" , data); } </script> <script src="http://localhost:1000/books?callback=getBooks" ></script> 响应 接收到了数据: {one : "沉默的大多数" , two : "尘埃落定" }
CORS 上面的JSONP需要客户端和服务端的代码配合,而CORS技术则只需要服务端调整代码即可。CORS通过在HTTP首部字段中添加相关参数,从而允许其他站点的浏览器向本机发出HTTP请求并返回响应。
另外,如果用CORS技术实现跨域,浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许跨域请求,服务端允许之后,才能发起实际的HTTP请求,服务端也可以通知客户但是否需要携带身份凭证,包括Cookies等HTTP认证数据,预请求是浏览器自动完成的,不需要用户参与。
缺点:允许多源则不可以携带Cookies,只能在允许一个源客户端和允许所有之间选择
CORS实现跨域,前端无需修改,代码主要几种在后端,这里的前端代码没有使用ajax,而是用原生 JS写了Ajax请求。
frontend:
1 2 3 4 5 6 7 var URL = "http://localhost:1000/books" let invocation = new XMLHttpRequest();invocation.open("GET" , URL, true ); invocation.send(); invocation.onload = () => { console .log(invocation.responseText); }
backend: 后端代码通过给HTTP首部添加字段,让服务器允许跨域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const express = require ("express" );const app = express();const http = require ("http" ).createServer(app);const PORT = 1000 ;const cors = require ("cors" );app.use(express.static('src' )); app.use((req, res, next ) => { res.header("Access-Control-Allow-Origin" , "http://127.0.0.1:5500" ); res.header("Access-Control-Allow-Credentials" , true ); res.header("Access-Control-Allow-Headers" , "Content-Type,Content-Length,Authorization,Accept,X-Requested-With" ); res.header("Access-Control-Allow-Methods" , "PUT,POST,GET,DELETE,HEAD,OPTIONS" ); if (req.method === 'OPTIONS' ) { res.sendStatus(200 ); return ; } next(); }); app.get("/books" , (req, res ) => { res.send("SUCCESS!" ) }) http.listen(PORT, () => { console .log(`listen on http://localhost:${PORT} ` ); })
HTTP Proxy 代理的方式可以实现跨域,主要用到的技术是Webpack,主要分为以下步骤:
安装Webpack环境
编写JS代码,发起跨域请求(正常写,但是URL要写成/api/books
的形式,不加HOST)
在webpack.config.js中配置proxy代理,将JS请求换源,以target为origin发送出去(其中还可以对URL进行操作)
编写后端代码(正常写)
所以我们需要安装一下Webpack的工程
1 2 3 4 5 6 mkdir htpx cd htpxyarn add webpack@4.44.1 webpack-cli@3.3.1 webpack-dev-server@3.11.0 html-webpack-plugin touch index.js touch index.html touch webpack.config.js
编写Webpack配置项,对devServer添加代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const HtmlPlguin = require ("html-webpack-plugin" );module .exports = { entry: './index.js' , devServer: { port: 3000 , progress: true , proxy: { '/api' : { target: 'http://localhost:1000' , pathRewrite: {'^/api' : '' }, changeOrigin: true } } }, plugins: [new HtmlPlguin({template : "index.html" })] }
前端代码:
1 2 3 4 5 6 7 8 9 document .getElementById("btn" ).addEventListener('click' , () => { var URL = "/api/books" let invocation = new XMLHttpRequest(); invocation.open("GET" , URL, true ); invocation.send(); invocation.onload = () => { console .log(invocation.responseText); } })
后端代码:
1 2 3 4 5 6 7 8 9 10 11 12 const express = require ("express" );const app = express();const http = require ("http" ).createServer(app);const PORT = 1000 ;app.get("/books" , (req, res ) => { res.send("SUCCESS!" ) }) http.listen(PORT, () => { console .log(`listen on http://localhost:${PORT} ` ); })
Nginx 后端代码需要改一下允许访问的IP,我们改为0.0.0.0
,即允许所有IP访问,其他保持一致
1 2 3 4 5 6 7 8 9 10 11 12 13 const express = require ("express" );const app = express();const http = require ("http" ).createServer(app);const PORT = 1000 ;app.get("/books" , (req, res ) => { res.send("SUCCESS!" ) }) http.listen(PORT,"0.0.0.0" , () => { console .log(`listen on http://talk.ymlog.cn:${PORT} ` ); })
前端代码和原来一样,只是修改一下里面的URL:
1 2 3 4 5 6 7 8 9 document .getElementById("btn" ).addEventListener('click' , () => { var URL = "http://talk.ymlog.cn:1000/books" let invocation = new XMLHttpRequest(); invocation.open("GET" , URL, true ); invocation.send(); invocation.onload = () => { console .log(invocation.responseText); } })
尝试着访问,发现跨域导致异常,下面开始配置nginx。
配置nginx 并开启 node 程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 user nginx;worker_processes auto;pid /run/nginx.pid;include /usr/share/nginx/modules/*.conf ;events { worker_connections 1024 ; } http { include /etc/nginx/mime.types; default_type application/octet-stream; upstream nodeapp { server talk.ymlog.cn:1000 ; } server { listen 80 default_server; location / { proxy_pass http://nodeapp; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' ; add_header Access-Control-Allow-Headers 'Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization' ; if ( $request_method = 'OPTIONS' ){ return 204 ; } } } }
前端代码中,需要把请求的host改为:
1 var URL = "http://talk.ymlog.cn/books"
综上就是nginx跨域的详细信息,就是用nginx给http报文添加头部信息,从而让浏览器允许跨域行为。
postMessage postMessage方法可以向其他窗口发送一个MessageEvent消息,接收窗口可以调用”message”监听事件获取到该消息,比如有两个页面,分别为index1.html和index2.html,由1向2发送消息,2接收1传来的消息,2接收到1的消息并打印到控制台
[send]index1.html:http://localhost:5500/index1.html
1 2 3 4 5 6 7 8 9 10 11 <body > <iframe src ="http://localhost:1000/index2.html" style ="display: none;" id ="iframe1" > </iframe > <script > let f = document .getElementById("iframe1" ); f.onload = function ( ) { f.contentWindow.postMessage('hello, I am index1' , 'http://localhost:1000/' ) } </script > </body >
[recv]index2.html:http://localhost:1000/index2.html
1 2 3 4 5 6 7 8 9 10 11 12 <body > <h1 > hi iframe2 </h1 > <iframe style ="display: none;" id ="iframe2" > </iframe > <script > window .addEventListener("message" , (event ) => { console .log(event.data); }) </script > </body >
WebSocket WebSocket自带跨域的功能,因为WebSocket只在和服务器建立连接时采用HTTP协议,而当连接建立之后,采用WS://
或WSS://
进行通信;浏览器的跨域拦截主要是通过过滤HTTP的报头来实现的,但是WebSocket不使用HTTP进行数据传递,所以浏览器没有拦截WebSocket数据的能力。
WebSocket 是一个全新的、独立的协议,基于 TCP 协议,与 HTTP 协议兼容却不会融入 HTTP 协议,仅仅作为 HTML5 的一部分。相比于传统的HTTP每次“请求-应答”,Websocket采取长连接,一旦连接建立,除非一方主动断开连接,否则可以一致以较小的开销传递数据,且是全双工的方式传递(WebSocket的Client和Server都可以向对方主动发送和接收数据)。
目前WebSocket对浏览器的兼容性不是很好,Socket.IO基于Node和JavaScript封装了WebSocket,包含Client和Server,但在这里,我们还是写原生的WebSocket
Frontend:
1 2 3 4 5 6 7 8 9 10 11 12 13 var ws = new WebSocket("ws://localhost:1000" );ws.onopen = function (res ) { ws.send("Hi" ); console .log("连接成功!" ); } ws.onmessage = function (res ) { console .log(res.data); }
Backend:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # yarn add websocket 安装websocket const express = require ("express" );const app = express();const http = require ("http" ).createServer(app);const WebSocket = require ("websocket" ).server;const PORT = 1000 ;http.listen(PORT, () => { console .log(`listen on http://localhost:${PORT} ` ); }) let ws = new WebSocket({ httpServer: http, autoAcceptConnections: false }); ws.on("request" , (request ) => { let connect = request.accept(null , request.origin); console .log(`A new connection on` ) setInterval (()=> { connect.sendUTF(new Date ()); },1000 ) })
Reference [1] https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
[2] https://www.ruanyifeng.com/blog/2016/04/cors.html
[3] https://github.com/amandakelake/blog/issues/62
[4] https://zhuanlan.zhihu.com/p/23467317
[5] https://github.com/chimurai/http-proxy-middleware
[6] https://juejin.im/post/6844904161369128967
[7] https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
[8] http://jartto.wang/2019/06/11/post-message/
[9] https://icewind-blog.com/2014/08/22/the-principle-of-javascript-prohibits-cross-domain/
[10] https://blog.securityevaluators.com/websockets-not-bound-by-cors-does-this-mean-2e7819374acc
[11] https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
[12] https://www.ruanyifeng.com/blog/2017/05/websocket.html