一次前后端联调的过程中,后端发现无论它代码怎么写,都没有成功把cookie写进页面。事后发现是前端的锅,前端没有进行跨域处理。

于是这位前端同学,打算好好地学一下跨域。然后发现跨域的图谱由点到面,竟不只有一个知识点要学。 由点到面理解跨域 - 图1image.png

一、什么叫跨域?跨域问题又从何说起?

当我们要跨过某一样的东西的时候,我们可以确认的是——肯定有东西在阻挡。
这里发起阻挡的对象是——浏览器。
浏览器出于安全的目的,它不允许不满足“同源策略”的站点A访问站点B的资源。
“跨域问题”其实更应该叫做“跨源问题”。

1.1“同源策略”—何谓同源,同源策略限制了什么

(他就是一个类似资源访问“白名单”的东西)

  • 协议一样:例如都是http
  • 域名一样:例如都是 www.baidu.com
  • 端口一样:例如都是8080端口,或者都是80默认端口

    以上三者也是们的URL的组成部分之一(协议+域名+端口+资源文件名字)

浏览器的“同源策略”限制了不同源之间的交互(例如发请求XMLHttpRequest)。

但这里必须注意一点:例如在JS脚本内发请求,“同源策略”对请求**限制的是,不让发起方获得请求结果,而不是直接阻断发起方——发起请求——这意味着实际上不同源的服务器,收到了请求,只是返回的结果,被浏览器阻断了。

浏览器的策略本质是:一个域名下面的JS,没有经过允许是不能读取另外一个域名的内容,但是浏览器不阻止你向另外一个域名发送请求。

MDN完整同源策略定义:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

里面有更细的内容,关于源的继承、源的更改。 迫于其中细节不是特别重点,这里不阐述。

以下为MDN-同源截图:
image.png

1.2 谁受“同源策略”的影响

  • 【DOM操作】(例如嵌入页面的iframe页面,默认不允许操作原页面的dom结构)
  • 【网络请求】XMLHttpRequset/Fetch

    可以正常发送请求,但是浏览器会阻断服务器的返回信息

  • 【数据——LocalStorage、SessionStorage、indexedDB、Cookie】不能跨域读取

    1.3 谁不受“同源策略”的影响

    以下的不受主要是,一开始指定策略的时候 Chrome 可能考虑了灵活性与安全性的权衡。

  • 【一些跨域的写操作】例如 location 的重定向、a标签资源跳转

  • 【一些跨域资源嵌入标签】script、img、video对资源的引用

    例如 由点到面理解跨域 - 图4

  • 【一些未被考虑进去的协议】websocket

二、为什么我们常常会遇见跨域的场景

作为前端,很多时候,我们与后端联调的时候,是需要跨域的。
因为通常,我们前端的项目,跑在我们自己本机的IP上,比如
172.17.191.1:8080
而后端同学他们的后端代码,通常也是跑在本机
172.17.191.33:8080
哪怕是跑在测试环境(测试服务器),那IP肯定也和我们前端同学不一样
test.hh.com
两个人的电脑就是两个IP地址,所以,如果你作为前端,用**你本机的项目**,与后端同学的项目(或者与服务器跑起来的项目),必然会遭遇跨域问题

(如果你没有遭遇过,说明该问题被下面的解决方案解决了)

**
当你发现有以下情况,可能就是你(或者后端)的“跨域工作”没有做好

  • 前端访问不了后端的接口
  • 后端发现设置不了cookie

**

三、常用的跨域的解决方案(部分有具体代码)

其实这些解决方案可以非常好的分类

其实当理解了浏览器的同源策略后,我们就能发现、或者直观地得出几种解决方案了。

  • 针对网络请求
    • 既然浏览器的“同源策略”限制了,我们就想办法绕开浏览器的同源策略。(服务器代理、直接关闭Chrome的安全检查)
    • 既然存在不受“同源策略”的标签,我们利用这些标签不就行了吗!机智如我们!!(script-JSONP、img-图片探测)
    • 使用官方开的后门——“CORS”,为跨域而生,不说了吧,官方的。
    • 特殊:Websocket(设计之初,他就不受“同源策略”限制——游离三界外,不在五行中)
  • 针对DOM操作
    • 利用不受“同源策略”限制的,postMessage 由点到面理解跨域 - 图5

      3.1 关闭浏览器安全检查(绕过浏览器·让浏览器失效)

      我直接“好家伙”。不过这玩意儿并不适用于我们,你不可能要求所有用户都这么做。 掩耳盗铃。

我们先用 koa 开一个文件服务器

  1. const koa = require('koa')
  2. const koaStatic = require('koa-static')
  3. const app = new koa()
  4. app.use(koaStatic(__dirname))
  5. app.listen(3030)

然后在 http://127.0.0.1:3030/otherIndex.html 中写入下面的 js 代码

(注意,该otherIndex.html也要放在和上面的服务器文件在一起,因为我上面用的 __dirname)

  1. fetch('http://127.0.0.1:8002/test')
  2. .then(res => res.json())
  3. .then(data => console.log('data', data))

可以看到,我们用
源:http://127.0.0.1:3030 访问了 源:http://127.0.0.1:8002

源8002的服务器代码

  1. const koa = require('koa')
  2. const koaStatic = require('koa-static')
  3. const app = new koa()
  4. app.use(koaStatic(__dirname))
  5. app.use(async ctx => {
  6. console.log('收到了请求')
  7. ctx.body = 'test'
  8. })
  9. app.listen(8002)

这样的结果是什么呢?
如果我们没有关闭chrome的默认安全策略:
image.png
接下里,让我们对 chrome 的安全策略进行关闭

  • c盘(或者其他地方创建个用户数据文件夹)MyChromeDevUserData

image.png

  • 修改Chrome启动图标中的目标

    image.png
    C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe —disable-web-security —user-data-dir=C:\MyChromeDevUserData

  • 用这个修改版的Chrome快捷方式打开目标页面,看见这个提示就说明你chrome修改对了

image.png

  • 再次打开页面,我们先原来的跨域报错没有了,可以正常访问

image.png
image.png

3.2 使用服务器代理(真·绕过浏览器)

这里其实利用了服务器不受跨域限制的原理。 既然浏览器不让,我们就: 浏览器 -> 同域代理服务器 -> 目标服务器

我们启动一个Nginx服务器,配置下代理
nginx.conf

  1. server {
  2. listen 3030;
  3. server_name localhost;
  4. location ^~/my/{
  5. proxy_pass http://127.0.0.1:8002;
  6. }
  7. location / {
  8. root html;
  9. index otherIndex.html;
  10. }
  11. }

otherIndex.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. </body>
  11. <script>
  12. fetch('/my/test')
  13. .then(res => res.json())
  14. .then(data => console.log('data', data))
  15. </script>
  16. </html>

开启Nginx服务器,然后我们发现,成功实现了跨域
image.png

3.3 JSONP(利用漏洞——不受“同源策略”影响的标签)

主要思路:

  • 创建一个script标签,通过修改 src 为我们get请求的路径,参数也传在 src 中,
  • 服务器对指定路由做处理(对接前端的src)
  • 拿到数据后删掉 script,收尾

前端代码

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. </body>
  11. <script>
  12. function jsonp({ url, params, callback }) {
  13. return new Promise((resolve, reject) => {
  14. let script = document.createElement('script')
  15. // 定义一个全局方法来接收,等后端返回-> 则执行,同步执行后,删除多创建的 script
  16. window[callback] = function (data) {
  17. console.log('看看后端传回的数据:', data)
  18. resolve(data)
  19. document.body.removeChild(script)
  20. }
  21. params = { ...params, callback }
  22. let arrs = []
  23. for (let key in params) {
  24. arrs.push(`${key}=${params[key]}`)
  25. }
  26. script.src = `${url}?${arrs.join('&')}`
  27. document.body.appendChild(script)
  28. })
  29. }
  30. jsonp({
  31. url: 'http://localhost:8002/say',
  32. params: { data: '前端传点什么呢' },
  33. callback: 'show'
  34. }).then(data => {
  35. console.log(data)
  36. })
  37. </script>
  38. </html>

koa代码

  1. let koa = require('koa')
  2. let app = new koa()
  3. app.use(async ctx => {
  4. if (ctx.path === '/say') {
  5. // 接收前端传回的参数
  6. const {callback} = ctx.query
  7. let params = {
  8. data: '后端想传过去的数据'
  9. }
  10. ctx.body = `${callback}(${JSON.stringify(params)})`
  11. }
  12. })
  13. app.listen(8002)

最终效果: http://127.0.0.1:3030/jsonpClient.html -> 跨域 -> http://localhost:8002/say
成功!!!
image.png

3.3.other 弱化版,利用 img 标签

这里提一嘴,这里也有弱化版,利用图片,但是只能发送请求,拿不到返回结果(无法像script一样,拿到结果,并利用结果来“执行函数”)
用来统计一下网站的访问次数(偷看用户当前浏览器的Cookie还是可以的)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. </body>
  11. <script>
  12. function jsonp({ url, params, callback }) {
  13. return new Promise((resolve, reject) => {
  14. let img = new Image()
  15. window[callback] = function (data) {
  16. document.body.removeChild(img)
  17. }
  18. img.onload = img.onerror = function(event) {
  19. console.log('请求返回了')
  20. window
  21. }
  22. params = { ...params, callback }
  23. let arrs = []
  24. for (let key in params) {
  25. arrs.push(`${key}=${params[key]}`)
  26. }
  27. img.src = `${url}?${arrs.join('&')}`
  28. document.body.appendChild(img)
  29. })
  30. }
  31. jsonp({
  32. url: 'http://localhost:8002/say',
  33. params: { data: '前端传点什么呢' },
  34. callback: 'show'
  35. }).then(data => {
  36. console.log(data)
  37. })
  38. </script>
  39. </html>

3.4 Websocket(利用漏洞——不受“同源策略”影响的协议)

启动一个 WebSocket 服务器

  1. const Koa = require('koa');
  2. const WebSocket = require('ws');
  3. const app = new Koa();
  4. const ws = new WebSocket.Server({ port: 8888 });
  5. ws.on('connection', ws => {
  6. console.log('server connection');
  7. ws.on('message', msg => {
  8. console.log('server receive msg:', msg);
  9. });
  10. ws.send('Information from the server');
  11. });
  12. app.listen(8002)

启动一个文件静态服务器,打开在 http://127.0.0.1:3030/otherIndex.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. </body>
  11. <script>
  12. let socket = new WebSocket('ws://127.0.0.1:8888')
  13. socket.onopen = function () {
  14. console.log('socket open')
  15. }
  16. socket.onmessage = function(event) {
  17. console.log('data', event.data)
  18. }
  19. socket.onerror = function (error) {
  20. console.log('error:', error)
  21. }
  22. </script>
  23. </html>

我们发现,
http://127.0.0.1:3030 -> WebSocket -> ws://127.0.0.1:8888
成功了!
说明跨域成功。

image.png

3.5 CORS(官方推出的跨域方案)

3.6 【针对DOM】的跨域,postMessage

  1. <script>
  2. var iframe = document.getElementById('iframe');
  3. iframe.onload = function() {
  4. var data = {
  5. name: 'aym',
  6. type:'wuhan'
  7. };
  8. // 向domain2传送跨域数据
  9. iframe.contentWindow.postMessage(JSON.stringify(data), 'http://10.73.154.73:8088');
  10. };
  11. // 接受domain2返回数据,这边给延迟的原因,因为同步传输时,页面不一定立马拿到数据,所以给延迟
  12. setTimeout(function(){
  13. window.addEventListener('message', function(e) {
  14. alert('data from domain2 sss ---> ' + e.data);
  15. }, false);
  16. },10)
  17. </script>

简单地来说,就是一边使用 postMessage 来发送。
一边通过监听 addEventListener - message 来接收。

通常这里,我们是会判断 origin 的来源的,不然不好区分到底是谁来跨的域。

四、跨域数据安全问题与CSRF攻击

4.1 HTTP是无状态的与Cookie、Session

4.2 Cookie的安全性问题

4.3 为什么Cookie有问题,却没有人提出对规则进行修改

4.4 利用Cookie的问题,进行CSRF攻击

相关阅读

MDN-CORS(跨域资源共享)——[https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS]
知乎文章——九种跨域——[https://zhuanlan.zhihu.com/p/55869398]