详解跨域

跨域问题的场景和解决方案多种多样,只要是做前端开发,总会遇到。而且面试时也是必问的问题。所以自己学习总结记录一下。

前置知识:浏览器的同源策略

首先需要明确一点:协议、域名、端口都相同才叫同源。
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
受到同源限制:
(1)无法读取不同源的 Cookie、LocalStorage 和 IndexDB 。
(2)无法获得不同源的DOM 。
(3)不能向不同源的服务器发送ajax请求。
不受同源限制:
在浏览器中,<script>、<img>、<iframe>、<link>等标签都可以跨域加载资源,而不受同源策略的限制。
浏览器对跨域访问的判定:
CORS机制把跨域请求分为两类:简单请求和非简单请求。
以下条件构成了简单请求:

  • Method: 请求的方法是 GET、POST 及 HEAD
  • Header: 请求头是 Content-Type (有限制)、Accept-Language、Content-Language 等
  • Content-Type: 请求类型是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain

非简单请求一般需要开发者主动构造,在项目中常见的 Content-Type: application/json 及 Authorization: <token> 为典型的「非简单请求」。
与之有关的三个字段如下:

  • Access-Control-Allow-Methods: 请求所允许的方法, 「用于预请求 (preflight request) 中」
  • Access-Control-Allow-Headers: 请求所允许的头,「用于预请求 (preflight request) 中」
  • Access-Control-Max-Age: 预请求的缓存时间

简单请求与非简单请求

简单请求:
浏览器会带上Origin的请求头发送到服务器,服务器根据Origin判断是否许可。如果许可就会带上CORS相关想要头,如果不在许可范围内就不会带上CORS相关的响应头。浏览器再根据响应头中是否有相关的CORS响应头,来判断拦截响应body和抛出错误。
非简单请求:
非简单请求会在发真正的请求之前发送一个OPTIONS的带着Origin、Access-Control-Request-Method、Access-Control-Request-Headers等CORS相关的请求头的预检请求到服务器,服务器确认可以这样请求,就会返回带着Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等CORS相关的响应头的响应,浏览器检查到相关的CORS响应头,说明通过预检可以继续发送真正的请求;服务器确认不可以,则不会返回这些相关响应头,浏览器没检查到CORS的响应头就会抛出错误。

关于跨域的几个问题

为什么a.wang.com访问wang.com也算跨域?
因为历史上,出现过不同的公司共用域名,a.wang.com和wang.com不一定是同一个网站,浏览器谨慎起见,认为这是不同的源。
为什么不同端口也算跨域?
原因同上,一个端口一个公司的情况也不是没有的。
记住:安全链条的强度取决于最弱的一环,所有和安全相关的问题都要谨慎对待。
为什么两个网站的IP一样,也算跨域?
原因同上,因为IP也是可以共用的。
为什么可以跨域使用CSS、JS和图片等?
同源策略限制的是数据访问,我们引用CSS、JS和图片的时候,其实并不知道其内容,我们只是在引用。

解决方案1:跨域资源共享(CORS)

从原理上讲实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
服务器要给接口的响应头设置:Access-Control-Allow-Origin:*
以koa框架举例,添加中间件,直接设置Access-Control-Allow-Origin请求头

app.use(async (ctx, next)=> {
  ctx.set('Access-Control-Allow-Origin', '*');
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200; 
  } else {
    await next();
  }
})

需要注意的是, Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host

解决方案2:JSONP跨域

我们在跨域的时候由于当前的浏览器不支持 CORS 或者因为某些条件不支持 CORS,我们必须使用另外一种方式来跨域,于是我们就请求一个 JS 文件,这个 JS 文件会执行一个回调,回调里面就有我们需要的数据。

let script = document.createElement('script');
script.src = 'http://www.wang.cn/login?username=wang&callback=callback';
document.body.appendChild(script);
function callback(res) {
  console.log(res);
}

jsonp的核心原理就是目标页面回调本地页面的方法,并带入参数
jsonp的跨域通信 就是利用script标签的异步加载实现的,只能发送get请求,客户端通过回调函数的形式处理相应数据
callback=jsonp 是和后台约定好的格式,具体名字可以自由约定
JSONP跨域优点
兼容ie并实现跨域
JSONP跨域缺点
由于是 script 标签,所以读不到 ajax 那么精确的状态,不知道状态码是什么,也不知道响应头是什么,它只知道成功和失败。
不支持post(因为是 script 标签,所以只支持 get 请求)
手写jsonp
手写jsonp并返回Promise对象
参数url,data:json对象,callback函数
原理:<script>元素不受同源策略的影响,可以进行AJAX传输。当script元素访问时,返回由回调函数进行包裹的json数据。在回调函数中获取数据进行处理。

function jsonp(url, data = {}, callback = 'callback') {
    // 处理json对象,拼接url
    data.callback = callback
    let params = []
    for (let key in data) {
        params.push(key + '=' + data[key])
    }
    console.log(params.join('&'))
    // 创建script元素
    let script = document.createElement('script')
    script.src = url + '?' + params.join('&')
    document.body.appendChild(script)
    // 返回promise
    return new Promise((resolve, reject) => {
        window[callback] = (data) => {
            try {
                resolve(data)
            } catch (e) {
                reject(e)
            } finally {
                // 移除script元素
                script.parentNode.removeChild(script)
                console.log(script)
            }
        }
    })
}

调用方法
1、创建script元素,设置src属性,并插入文档中,同时触发AJAX请求。
2、返回Promise对象,then函数才行继续,回调函数中进行数据处理
3、script元素删除清理

jsonp('http://photo.sina.cn/aj/index', {
  page: 1,
  cate: 'recommend'
}, 'jsoncallback').then(data => {
  console.log(data)
})

解决方案三:axios中解决跨域问题

使用axios直接进行跨域访问不可行,我们需要配置代理
代理可以解决的原因:
因为客户端请求服务端的数据是存在跨域问题的,而服务器和服务器之间可以相互请求数据,是没有跨域的概念(如果服务器没有设置禁止跨域的权限问题),也就是说,我们可以配置一个代理的服务器可以请求另一个服务器中的数据,然后把请求出来的数据返回到我们的代理服务器中,代理服务器再返回数据给我们的客户端,这样我们就可以实现跨域访问数据
1.配置BaseUrl

import axios from 'axios'
Vue.prototype.$axios = axios
axios.defaults.baseURL = '/api'  //关键代码

2.配置代理
在config文件夹下的index.js文件中的proxyTable字段中,作如下处理:

proxyTable: {
 '/api': {
   target:'http://api.douban.com/v2', // 你请求的第三方接口
   changeOrigin:true,

   pathRewrite:{  // 路径重写,
    '^/api': ''
   }
  }
}

上面代码会在本地创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,这样服务端和服务端进行数据的交互就不会有跨域问题。
同时pathRewrite会重写路径,在实际请求的时候去掉/api
原理:
因为我们给url加上了前缀/api,我们访问/movie/top250就当于访问了:localhost:8080/api/movie/top250(其中localhost:8080是默认的IP和端口)。
在index.js中的proxyTable中拦截了/api,并把/api及其前面的所有替换成了target中的内容,因此实际访问Url是api.douban.com/v2/movie。
至此,纯前端配置代理解决axios跨域得到解决

其他解决方案

通过服务端实现代理请求转发
以express框架为例

var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
                      }));
module.exports = app

另外还可以通过配置nginx实现代理

server {
    listen    80;
    # server_name xxx.xxx.com;
    location / {
        root  /var/www/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect   off;
        proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP     $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

Hash
通过hash实现跨域通信,假设当前是在页面A,通过iframe或frame嵌入了跨域的页面B,A页面的伪代码如下:

var B = document.getElementByTagName('iframe')
B,src = B.src + '#' + 'data'

在B中的伪代码如下:

window.onhashchange = function () {
  var data = window.location.hash
}

postMessage
postMessage是HTML5新提供的方法
窗口A(http:A.com)向跨域的窗口B(http://B.com)发送信息

window.postMessage( 'data', 'http://B.com' )

在窗口B实现监听

window.addEventListenner('message', function (event){
  console.log(event.origin)  //http://A.com
  console.log(event.source)  //Bwindow
  console.log(event.data)  //data
})

WebSocket可以实现跨域通信
CORS可以实现跨域通信
fetch是一种新的同源通信标准,使用效果和ajax差不多,cors就是在fetch的基础上添加相关的参数实现的。

fetch('/some/url', {
  method: 'get'
}).then(function(response){
}).catch(function(err){
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,527评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,314评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,535评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,006评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,961评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,220评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,664评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,351评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,481评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,397评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,443评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,123评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,713评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,801评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,010评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,494评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,075评论 2 341

推荐阅读更多精彩内容

  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    他方l阅读 1,056评论 0 2
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    Yaoxue9阅读 1,280评论 0 6
  • 1. 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScri...
    cbw100阅读 6,293评论 2 86
  • 题目1.什么是同源策略? 同源策略(Same origin Policy): 浏览器出于安全方面的考虑,只允许与本...
    FLYSASA阅读 1,704评论 0 6
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    HeroXin阅读 824评论 0 4