跨域
因为浏览器出于安全考虑,有同源策略。也就是说,如果
协议、域名、端口有一个不同就是跨域,Ajax请求会失败。
我们可以通过以下几种常用方法解决跨域的问题
JSONP
JSONP的原理很简单,就是利用<script>标签没有跨域限制的漏洞。通过<script>标签指向一个需要访问的地址并提供一个回调函数来接收数据
涉及到的端
JSONP 需要服务端和前端配合实现。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script><script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
JSONP使用简单且兼容性不错,但是只限于get请求
具体实现方式
- 在开发中可能会遇到多个
JSONP请求的回调函数名是相同的,这时候就需要自己封装一个JSONP,以下是简单实现
function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function (data) {
success && success(data)
}
document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function (value) {
console.log(value)
})function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function (data) {
success && success(data)
}
document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function (value) {
console.log(value)
})CORS
CORS(Cross-Origin Resource Sharing,跨域资源共享) 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求
涉及到的端
CORS只需要服务端/后端支持即可,不涉及前端改动
CORS需要浏览器和后端同时支持。IE 8和9需要通过XDomainRequest来实现。- 浏览器会自动进行
CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,就实现了跨域。 - 服务端设置
Access-Control-Allow-Origin就可以开启CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源
只要后端实现了 CORS,就实现了跨域

以 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()
}
})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()
}
})具体实现方式
CORS 将请求分为简单请求(Simple Requests)和需预检请求(Preflighted requests),不同场景有不同的行为
简单请求
:不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:
请求方法:
GET、HEAD、POST。请求头:
Accept、Accept-Language、Content-Language、Content-TypeContent-Type仅支持:application/x-www-form-urlencoded、multipart/form-data、text/plain
需预检请求:当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个
OPTIONS请求,通过服务端返回的Access-Control-Allow-*判定请求是否被允许
CORS 引入了以下几个以 Access-Control-Allow-* 开头:
Access-Control-Allow-Origin表示允许的来源Access-Control-Allow-Methods表示允许的请求方法Access-Control-Allow-Headers表示允许的请求头Access-Control-Allow-Credentials表示允许携带认证信息
当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求
nginx 反向代理
反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可
nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)
- 全局配置,在
nginx.conf文件中的http节点加入跨域信息
http {
# 跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}http {
# 跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}- 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
server {
listen 8080;
server_name server_name;
charset utf-8;
location / {
# 这里配置单个代理跨域,跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
#配置代理 代理到本机服务端口
proxy_pass http://127.0.0.1:9000;
proxy_redirect off;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}server {
listen 8080;
server_name server_name;
charset utf-8;
location / {
# 这里配置单个代理跨域,跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
#配置代理 代理到本机服务端口
proxy_pass http://127.0.0.1:9000;
proxy_redirect off;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Node 中间层接口转发
const router = require('koa-router')()
const rp = require('request-promise')
// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
let { category = 'trending', lang = 'javascript', limit, offset, period } = ctx.request.body
lang = lang || 'javascript'
limit = limit || 30
offset = offset || 0
period = period || 'week'
let res = await rp({
method: 'POST',
// 跨域的接口
uri: `https://e.juejin.cn/resources/github`,
body: {
category,
lang,
limit,
offset,
period,
},
json: true,
})
ctx.body = res
})
module.exports = routerconst router = require('koa-router')()
const rp = require('request-promise')
// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
let { category = 'trending', lang = 'javascript', limit, offset, period } = ctx.request.body
lang = lang || 'javascript'
limit = limit || 30
offset = offset || 0
period = period || 'week'
let res = await rp({
method: 'POST',
// 跨域的接口
uri: `https://e.juejin.cn/resources/github`,
body: {
category,
lang,
limit,
offset,
period,
},
json: true,
})
ctx.body = res
})
module.exports = routerProxy
如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域
在vue.config.js文件,新增以下代码
module.exports = {
devServer: {
host: '127.0.0.1',
port: 8080,
open: true, // vue项目启动时自动打开浏览器
proxy: {
'/api': {
// '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: {
// pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': '',
},
},
},
},
}module.exports = {
devServer: {
host: '127.0.0.1',
port: 8080,
open: true, // vue项目启动时自动打开浏览器
proxy: {
'/api': {
// '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: 'http://xxx.xxx.xx.xx:8080', //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: {
// pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': '',
},
},
},
},
}通过axios发送请求中,配置请求的根路径
axios.defaults.baseURL = '/api'axios.defaults.baseURL = '/api'此外,还可通过服务端实现代理请求转发,以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 = appvar 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 = appwebsocket
webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信
原理:利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。具体看以下代码:
function socketConnect(url) {
// 客户端与服务器进行连接
let ws = new WebSocket(url) // 返回`WebSocket`对象,赋值给变量ws
// 连接成功回调
ws.onopen = (e) => {
console.log('连接成功', e)
ws.send('我发送消息给服务端') // 客户端与服务器端通信
}
// 监听服务器端返回的信息
ws.onmessage = (e) => {
console.log('服务器端返回:', e.data)
// do something
}
return ws // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800') // websocket对象function socketConnect(url) {
// 客户端与服务器进行连接
let ws = new WebSocket(url) // 返回`WebSocket`对象,赋值给变量ws
// 连接成功回调
ws.onopen = (e) => {
console.log('连接成功', e)
ws.send('我发送消息给服务端') // 客户端与服务器端通信
}
// 监听服务器端返回的信息
ws.onmessage = (e) => {
console.log('服务器端返回:', e.data)
// do something
}
return ws // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800') // websocket对象document.domain(不常用)
- 该方式只能用于二级域名相同的情况下,比如
a.test.com和b.test.com适用于该方式。 - 只需要给页面添加
document.domain = 'test.com'表示二级域名都相同就可以实现跨域 - 自
Chrome 101版本开始,document.domain将变为可读属性,也就是意味着上述这种跨域的方式被禁用了
postMessage(不常用)
在两个 origin 下分别部署一套页面 A 与 B,A 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://test.com') {
console.log('验证通过')
}
})// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://test.com') {
console.log('验证通过')
}
})window.name(不常用)
主要是利用
window.name页面跳转不改变的特性实现跨域,即iframe加载一个跨域页面,设置window.name,跳转到同域页面,可以通过$('iframe').contentWindow.name拿到跨域页面的数据
实例说明
比如有一个www.example.com/a.html页面。需要通过a.html页面里的js来获取另一个位于不同域上的页面www.test.com/data.html中的数据。
data.html页面中设置一个window.name即可,代码如下
<script>
window.name = '我是data.html中设置的a页面想要的数据'
</script><script>
window.name = '我是data.html中设置的a页面想要的数据'
</script>- 那么接下来问题来了,我们怎么把
data.html页面载入进来呢,显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面(因为我们现在需要实现的是a.html页面不跳转,但是也能够获取到data.html中的数据) - 具体的实现其实就是在
a.html页面中使用一个隐藏的iframe来充当一个中间角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。 - 充当中间人的
iframe想要获取到data.html中通过window.name设置的数据,只要要把这个iframe的src设置为www.test.com/data.html即可,然后a.html想要得到iframe所获取到的数据,也就是想要得到iframe的widnow.name的值,还必须把这个iframe的src设置成跟a.html页面同一个域才行,不然根据同源策略,a.html是不能访问到iframe中的window.name属性的
<!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload="getData()">
<script>
function getData(){
var iframe = document.getElementById('proxy);
iframe.onload = function(){
var data = iframe.contentWindow.name;
//上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
}
iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
}
</script>
</iframe><!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload="getData()">
<script>
function getData(){
var iframe = document.getElementById('proxy);
iframe.onload = function(){
var data = iframe.contentWindow.name;
//上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
}
iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
}
</script>
</iframe>上面的代码只是最简单的原理演示代码,你可以对使用 js 封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe
扩展阅读
跨域与监控
前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈
跨域与图片
前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决
Cao Note