跨域与9种解决方案 简单请求复杂请求跨域 跨域的cookie携带问题
跨域
什么是跨域?
违反了浏览器同源策略的都是跨域
同源策略
何谓之同源?
同源即同协议、同域名、同端口,否则为跨域
同源策略会阻止一个域的JavaScript脚本和另外一个域的内容进行交互
跨域的表现
跨域例:
http://www.test.cn:3000
https://www.test.cn:3000
不同源时,以下操作会受影响的:
- js操作本地存储如cookie、LocalStorage
- js操作页面DOM元素
- 可以发送ajax请求并且被服务端正常响应,但响应结果会被浏览器拦截(Cross-Origin Read Blocking)
为什么浏览器不支持跨域
- 安全方面,防止窃取cookie。用户可能给恶意网站发请求,恶意网站拿到用户cookie
- DOM方面,如果可以操作DOM,可能嵌入iframe,造成安全问题
跨域的9种解决方案
- jsonp
- cors (cross origin resource sharing) 跨域资源共享
- postMessage
- document.domain(主域和副域名)
- window.name
- location.hash
- nginx
- websokcet(页面之间通信)
- http-proxy
1 jsonp
jsonp, 即JSON padding,跨域获取json的方式
浏览器拦截跨域请求,只针对js
因此,一些具有引入外部资源属性的标签,在引入时并不会被浏览器的同源机制拦截。在此基础上可以有如下方案:
- 通过link标签的href
- 通过img标签的src
- 通过script标签的src
下面给出基于script的实现方案
- 创建script标签,src属性指向外部资源
- 在src上指定回调函数,并将json数据携带在回调函数的参数上响应给客户端
- 客户端接收响应,将src请求回来的脚本字符串放在script标签体内执行,调用设置好的回调函数,获取数据
方式一: 本地开启服务,jsonp获取数据
html:
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//创建script标签,并引用外部资源
function jsonp(params) {
return new Promise(function (resolve, reject) {
const { url, query, cb } = params
const address = `${url}?wd=${query.wd}&cb=${cb}`
let script = document.createElement('script')
script.src = address
// 执行回调
window[cb] = function (data) {
resolve(data)
// 垃圾回收
document.body.removeChild(script)
}
document.body.appendChild(script)
})
}
const getJsonpRes = async function () {
let rs = await jsonp({
url: 'http://localhost:3000/say',
// 查询字段
query: { wd: 'island' },
// 回调函数
cb: 'getCRData'
})
console.log('result from server:',rs)
return rs
}
getJsonpRes();
</script>
</body>
</html>
server:
let express = require('express')
let app = express()
app.get('/say', function(req, res){
let {wd,cb} = req.query
console.log(wd,cb)
//将数据作为回调函数的参数返回
const jsonData = JSON.stringify([{name:"island"}])
res.end( `${cb}(${jsonData})` )
})
app.listen(3000, () =>console.log('server run at 3000'))
方式二:使用百度搜索的服务
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//创建script标签,并引用外部资源
function jsonp(params) {
return new Promise(function (resolve, reject) {
const { url, query, cb } = params
const address = `${url}?wd=${query.wd}&cb=${cb}`
let script = document.createElement('script')
script.src = address
// 执行回调
window[cb] = function (data) {
//返回数据
resolve(data)
// 垃圾回收
document.body.removeChild(script)
}
document.body.appendChild(script)
})
}
const getJsonpRes = async function () {
let rs = await jsonp({
url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
// 查询字段
query: { wd: 'island' },
// 回调函数
cb: 'getCRData'
})
console.log(rs)
return rs
}
const getJsonpRes2 = function () {
jsonp({
url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
query: { wd: 'island' },
cb: 'getCRData'
}).then(rs => {
console.log(rs)
})
}
// getJsonpRes();
// getJsonpRes2();
</script>
</body>
</html>
缺陷:
- 只支持get请求,不支持post、put、delete。
- 不安全,可能有xss攻击(可能返回一个script标签)
2 cors
cors,即cross origin resource sharing,跨域资源共享
这种方式主要是在后端实现,通过设置各种响应头,来保证响应数据不被浏览器拦截
客户端
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello world 3000
<script>
let xhr = new XMLHttpRequest()
// 第三个参数指定异步或者同步
// xhr.open('GET', 'http://localhost:4000/getData',true)
// (4) 强制带上凭证
document.cookie = 'name=island'
xhr.withCredentials = true
//(2) 可选设置请求头名字,如果设置了,服务端也要设置响应的Access-Control-Allow-Headers
// xhr.setRequestHeader('name','island')
// (3)
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.onreadystatechange = function () {
//304 缓存
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
console.log(xhr.response)
// (5) 获取返回头
console.log(xhr.getResponseHeader('name'))
}
}
xhr.send()
</script>
</body>
</html>
本地服务
const express = require('express')
const app = express()
//启用静态资源
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
外部服务
const express = require('express')
const app = express()
//设置访问白名单
let accessList = ['http://localhost:3000']
app.use(function (req, res, next) {
let origin = req.headers.origin
if (accessList.includes(origin)) {
//设置允许跨域访问服务的白名单,当设置为*的时候,设置携带cookie失效
res.setHeader('Access-Control-Allow-Origin', origin)
// 设置允许的请求头名字
res.setHeader('Access-Control-Allow-Headers', 'name')
//get post是默认支持的,不用配,其他的需要配置
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检options的存活时间,即都少秒之后重新预检
// res.setHeader('Access-Control-Max-Age',6)
//允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
//允许设置响应头
res.setHeader('Access-Control-Expose-Headers','name')
if (req.method === 'OPTIONS') {
//OPTIONS请求不做任何处理,因为是试探预检请求,确定接口正常才可以发送请求体
// res.end()
console.log('接收到了OPTIONS请求')
}
}
next()
})
app.get('/getData', function (req, res) {
console.log('接收到了请求')
res.end('data from 4000')
})
app.put('/getData', function (req, res) {
console.log('接收到PUT了请求')
// 设置响应头
res.setHeader('name', 'island header name')
res.end('data from 4000')
})
app.listen(4000, () => console.log('server run at 4000'))
3 postMessage
主要api:
iframe.contentWindow
window.postMessage
onmessage, e.soure
实现:
- 通过iframe嵌套需要发送信息的网页,并监听onload事件保证网页加载完毕
- 在load回调里通过iframe.contentWindow获取对方网页,使用iframe.contentWindow.
- postMessage向对方网页发送信息,对方网页通过window监听onmessage获取信息
- 对方网页在onmessage回调中,e.source.postMessage向我放网页发送信息
- 我方window监听onmessage,获取响应信息。实现了双向通信
本地网页
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 借用iframe+onload事件+postMessage -->
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()" frameborder="0"></iframe>
<script>
function load(params){
//和b窗口通信
let frame = document.getElementById('frame')
// 给b窗口发送信息,postMessage(data,url)
frame.contentWindow.postMessage('a send msg to b','http://localhost:4000')
//监听b的回复
window.onmessage = (e)=>{
console.log(e.data)
}
}
</script>
</body>
</html>
本地服务
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
对方网页
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
this is b.html
<script>
window.onmessage = (e)=>{
console.log(e.data, e)
//回复给a窗口
e.source.postMessage('b send msg to a', e.origin)
}
</script>
</body>
</html>
对方服务
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))
4 window.name
思路:
iframe+onload
虽然iframe可以通过src引入外部网页,但是不能拿到外部网页的DOM或者属性,因为这是跨域的
因此可以在第一次onload时,改变src,切换到同源网页,此时name属性依然还在,完成跨域获取数据
iframe引用外部网页,在外部网页中设置window.name属性
iframe监听onload事件,第一次触发onload事件时,改变iframe的src指向同源的window,并toggle bool
iframe第二次触发onload事件,拿到iframe.contentWindow.name
本地页面
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
a和b是同域的 3000
c是独立的 4000
a获取c的数据
a先引用c c把值放到window.name,把a页面的iframe的引用地址改到b,name不会消失(标签没变,只是标签的属性变化,所以src变化window.name依然还在)
<iframe id="frame" src="http://localhost:4000/c.html" onload="load()" frameborder="0"></iframe>
<script>
let first = true
function load() {
let frame = document.getElementById('frame')
//虽然可以通过src引入外部网页,但是不能拿到外部网页的DOM或者属性,因为这是跨域的
// console.log(frame.contentWindow.name)
// 初次进入,加载b页面
if (first) {
frame.src = 'http://localhost:3000/b.html'
first = false
return
} else {
console.log(frame.contentWindow.name)
}
}
</script>
</body>
</html>
本地页面b,作为中间桥梁
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
this is b
</body>
</html>
本地服务,提供静态资源访问
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
外部网页
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
window.name = '这是4000下, c中的window name'
</script>
</body>
</html>
外部服务,提供静态资源访问
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))
5 hash
思路:
- a通过iframe引用c,并在url后面设置hash值hasha,监听hashchange事件
- c用loaction.hash收到hasha之后,通过iframe引用和a同源的b,并在iframe的src上设置hash值hashc
- b通过location.hash拿到hashc,并通过window.parent.parent拿到窗口a,给a设置hashc,设置完之后,a的hashchange事件触发,拿到c传过来的hash值
本地服务和外部服务
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(3000, ()=>console.log('server run at 3000'))
const express = require('express')
const app = express()
app.use(express.static(__dirname))
app.listen(4000, ()=>console.log('server run at 4000'))
本地网页
<! DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
page a
<!--
路径后面的hash值可以用来通信
a和b同
c独立域
-->
<!-- 目的a想访问c -->
<!-- a给c传一个hash值,c收到hash值后把hash值传给b -->
<!-- c给hash值传给b, b将结果放到a的结果中 -->
<iframe src="http://localhost:4000/c.html#req_island" frameborder="0"></iframe>
<script>
window.onhashchange = function(){
console.log(location.hash)
}
</script>
</body>
</html>
和本地网页同源的网页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
this is b
<script>
window.parent.parent.location.hash = location.hash
</script>
</body>
</html>
外部网页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--
c拿到之后创建iframe指向b
b和a同域
-->
<script>
console.log('a传给c的hash是:',location.hash)
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:3000/b.html#res_island'
document.body.appendChild(iframe)
</script>
</body>
</html>
6 domain
domain主要用于一级域名和二级域名之间的跨域通信
只要一级域名和二级域名之间设置document.domain的值相同
就可以通过iframe.contentWindow拿到对方的window的属性实现通信
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--
一级域名二级域名
思路:
a通过http://a.island.cn:3000/b.html:3000/a.html
b是通过http://b.island.cn:3000/b.html:3000/b.html
通过iframe,再用document.domain声明是一家的域名,就可以访问了
局限是主域名和副域名之间可以用
-->
hello a
<iframe id="frame" src="http://b.island.cn:3000/b.html" frameborder="0" onload="load()"></iframe>
<script>
//2 domain,告诉是一家的
document.domain = 'island.cn'
function load(){
let frame = document.getElementById('frame')
// 拿到b窗口的window下属性,跨域
frame.contentWindow.propsb
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello b
<script>
document.domain = 'island.cn'
window[propsb] = 'this is propsb from b'
</script>
</body>
</html>
7 websocket
不同于基于JavaScript的ajax,websocket没有跨域限制
websocket协议是ws,内部基于tcp
socket和http的本质区别在于,socket是双向的,http是单向的
作为h5的高级API,websocket的一个缺陷是兼容性不太好,但是有一个常用的库兼容性很强,即socket.io
建立websocket通信
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
//新建websocket通信
let socket = new WebSocket('ws://localhost:3000')
//给websocket服务器发送信息
socket.onopen = function(){
socket.send('island send')
}
//监听socket服务器返回的信息
socket.onmessage = function(e){
console.log(e.data)
}
</script>
</body>
</html>
建立websocket服务
yarn add ws
let Websocket = require('ws')
let wss = new Websocket.Server({port:3000})
wss.on('connection',function(ws){
ws.on('message',function(data){
console.log(data)
ws.send('island server response')
})
})
8 Nginx 配置跨域
nginx跨域是最简单的跨域
配置nginx.conf
//所有json后缀的,在nginx文件夹的json目录下查找
location ~..json{
# 指定根目录下 json目录
root json;
# 添加跨域头,此来源将被列入白名单
add_header "Access-Control-Allow-Origin" ""
}
9 webpack配置跨域
webpack可以在devServer中配置跨域,常见配置如下
target:表示目标资源的地址
pathRewrite:对该字段重写
changeOrigin:核心配置。为true时会将请求头中的host字段改成target
secure:设置https协议的代理,因为target默认不支持https协议,因此需要做额外的配置