浏览器同源策略 浏览器同源策略是一个安全策略,其中同源指的是 协议 + 域名 + 端口号
三者相同,即使有两个不同的域名指向同一个 IP 地址,也不是同源的。同源策略可以一定程度上防止 XSS、CSRF 攻击。
一个域名的组成包括:
在默认情况下 http 可以省略端口 80, https 可以省略端口 443。也就是说,https://www.baidu.com 和 https://www.baidu.com:443 显然也是同源的,因为它们是一回事。
不符合同源策略导致的后果有:
localStorage、sessionStorage、Cookie 等浏览器的内存无法跨域访问 DOM 节点无法进行跨域操作 Ajax 请求无法跨域请求 但是有一些标签是允许跨域加载资源:
值得注意的几个要点有:
如果是协议和端口造成的跨域问题,前端是无能为力的 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了 代码示例 1 2 3 4 5 6 7 8 9 10 const http = require ('http' );const port = 8000 ;http.createServer((req, res ) => { res.end(JSON .stringify('hello world' )); }).listen(port, function ( ) { console .log('server is listening on port ' + port); })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script > var xhr = new XMLHttpRequest(); xhr.open('get' , 'http://127.0.0.1:8000' ); xhr.send(); xhr.onreadystatechange = function ( ) { if (xhr.readyState === 4 && xhr.status === 200 ) { console .log(xhr.responseText); } } </script >
果不其然报错了:
JSONP JSONP,即 JSON with Padding,是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 get 请求。 JSONP 工作原理:在网页有一些标签天生就具有跨域能力,比如 img
link
script
等。JSONP 就是利用 script
标签的跨域能力来发送请求的。
举个例子,客户端传入 a 和 b,服务端传回 a + b 的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const http = require ('http' );const url = require ('url' );const port = 8000 ;http.createServer((req, res ) => { const { query } = url.parse(req.url, true ); const { a, b, callback } = query; const ans = parseInt (a) + parseInt (b); res.end(`${callback} ('${ans} ')` ); }).listen(port, () => { console .log('server is listening on port ' + port); })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <body > </body > <script > function add (ans ) { console .log('a + b =' , ans); } const jsonp = (a, b, callback ) => { const script = document .createElement('script' ); script.src = `http://127.0.0.1:8000?a=${a} &b=${b} &callback=${callback} ` ; document .body.appendChild(script); } jsonp(1 , 2 , 'add' ); </script >
缺点:需要前后端配合,只支持 get 请求。
CORS CORS 全称是 Cross-Orgin Resource Sharing,跨域资源共享。CORS 由后端开启,开启后前端就可以跨域访问后端。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置为通配符 *,则表示所有网站都可以访问资源。类似的还有 Access-Control-Allow-Methods
,表示允许的请求方法,Access-Control-Allow-Headers
,表示允许的请求头类型。
设置 CORS 本身和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求 和复杂请求 。
简单请求 只要同时满足以下条件的就是简单请求:
请求方法是以下三者之一: 允许人为设置的字段仅限以下几种: Accept Accept-Language Content-Language Content-Type(有额外限制) Content-Type 取值为以下三者之一: text/plain multipart/form-data application/x-www-form-urlencoded 请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
请求中没有使用 ReadableStream 对象。
复杂请求 不是简单请求的请求就是复杂请求。复杂请求 必须首先使用 OPTIONS
请求方法发起一个预检请求 到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生预期之外的影响。
代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script > var xhr = new XMLHttpRequest(); xhr.open('get' , 'http://127.0.0.1:8000?a=1&b=2' ); xhr.send(); xhr.onreadystatechange = function ( ) { if (xhr.readyState === 4 && xhr.status === 200 ) { console .log(xhr.responseText); } } </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const http = require ('http' );const url = require ('url' );const port = 8000 ;http.createServer((req, res ) => { res.writeHead(200 , { 'Access-Control-Allow-Origin' : 'http://127.0.0.1:5500' , "Access-Control-Allow-Methods" : "DELETE,PUT,POST,GET,OPTIONS" , 'Access-Control-Allow-Headers' : 'Content-Type' }) const { query : { a, b } } = url.parse(req.url, true ); res.end(`${a} + ${b} = ${parseInt (a) + parseInt (b)} ` ); }).listen(port, function ( ) { console .log('server is listening on port ' + port); })
WebSocket Websocket 属于应用层协议,它基于 TCP 传输协议,并复用 http 的握手通道。 相比于 http 协议,它的优点是:
支持双向通信,客户端和服务器之间存在持久的连接,而且双方都可以随时开始发送数据 有更好的二进制支持 支持拓展 因为这种方式本质没有使用 http 的响应头, 因此也没有跨域的限制。
那么为什么 WebSocket 可以跨域呢? 因为 WebSocket 根本不属于同源策略,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,但是 WebSocket 出现以后,对于 WebSocket 的跨域检测工作就交给了服务端,浏览器仍然会带上一个 Origin 跨域请求头,服务端则根据这个请求头判断此次跨域 WebSocket 请求是否合法。
依然以 a + b 问题举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script > function ws (a, b ) { const socket = new WebSocket('ws://127.0.0.1:8000' ); socket.onopen = () => { socket.send(JSON .stringify({ a, b })); }; socket.onmessage = e => { console .log(e.data); } } ws(1 , 2 ); </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 const WebSocket = require ('ws' );const port = 8000 ;const ws = new WebSocket.Server({ port });ws.on('connection' , obj => { obj.on('message' , data => { data = JSON .parse(data.toString()); const { a, b } = data; obj.send(`${a} + ${b} = ${parseInt (a) + parseInt (b)} ` ); }) })
Node 接口代理 同源策略只在浏览器存在,无法限制后端。也就是说前端与后端之间会受同源策略影响,但是后端与后端之间不会受到限制。所以可以通过 Node 做一层接口代理,先访问已经设置了 CORS 的后端 1,再让后端 1 去访问后端 2,获取数据后传给后端 1,最后再让后端 1 把数据传回给前端。
客户端代码同上,把请求端口改成 8888 即可。
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 33 34 35 36 37 const http = require ('http' );const url = require ('url' );const querystring = require ('querystring' );const port = 8888 ;http.createServer((req, res ) => { res.writeHead(200 , { 'Access-Control-Allow-Origin' : 'http://127.0.0.1:5500' , "Access-Control-Allow-Methods" : "DELETE,PUT,POST,GET,OPTIONS" , 'Access-Control-Allow-Headers' : 'Content-Type' }) const { query } = url.parse(req.url, true ); const { methods = 'GET' , headers } = req; http.request({ host : '127.0.0.1' , port : '8000' , path : `/?${querystring.stringify(query)} ` , methods, headers }, proxyRes => { proxyRes.on('data' , chunk => { console .log(chunk.toString()); res.end(chunk.toString()); }) }).end() }).listen(port, () => { console .log('server is listening on port ' + port); })
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const http = require ('http' );const url = require ('url' );const port = 8000 ;http.createServer(function (req, res ) { res.writeHead(200 , { 'Access-Control-Allow-Origin' : 'http://127.0.0.1:5500' , "Access-Control-Allow-Methods" : "DELETE,PUT,POST,GET,OPTIONS" , 'Access-Control-Allow-Headers' : 'Content-Type' }) const { query : { a, b } } = url.parse(req.url, true ); res.end(`${a} + ${b} = ${parseInt (a) + parseInt (b)} ` ); }).listen(port, function ( ) { console .log('server is listening on port ' + port); })
Nginx 反向代理 实现原理类似于上面提到的 Node 接口代理,需要你搭建一个中转 Nginx 服务器,用于转发请求。使用 Nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 Nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器的性能。
先根据Nginx安装教程 进行 Nginx 的安装。 然后修改 conf 目录下的 nginx.conf 文件:
1 2 3 4 5 6 7 8 server{ listen 8888; server_name 127.0.0.1; location /{ proxy_pass 127.0.0.1:8000; } }
输入命令行
此时客户端请求 8888 端口,就不会跨域了。
参考资料:https://juejin.cn/post/7017614708832206878 https://juejin.cn/post/6844904126246027278 https://juejin.cn/post/6844903767226351623 https://www.jianshu.com/p/9a8d793ec52a https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS 公众号前端点线面