跨域问题的场景和解决方案多种多样,只要是做前端开发,总会遇到。而且面试时也是必问的问题。所以自己学习总结记录一下。
前置知识:浏览器的同源策略
首先需要明确一点:协议、域名、端口都相同才叫同源。
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况: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){
})