解决跨域的方式
什么是跨域?怎么解决跨域?什么是预检请求?
跨域:协议、域名、端口有一个不同就是跨域。
浏览器出于安全考虑,使用了同源策略;同源策略是一种约定,它是浏览器最核心也最基本的安全功能。
那么是出于什么安全考虑才引入这种机制呢?主要是为了防止XSS
、 CSRF
攻击。CSRF 攻击是利用用户的登录态发起恶意请求。
也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问内容,如果你当前 A 网站已经登录,那么对方就可以通过 Ajax 获得你的任何信息。
同源策略限制内容有:
Cookie
、LocalStorage
、IndexedDB
等存储性内容DOM
节点AJAX
请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源:
<img src=XXX>
<link href=XXX>
<script src=XXX>
请求跨域了,那么请求到底发出去没有?
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取另一个域名下的内容, Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF ,因为请求毕竟是发出了。
JSONP
JSONP
的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。通过 <script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
<script src="http://test/api?param1=ab&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
优点:简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题
缺点:仅支持 get
方法具有局限性,不安全可能会遭受 XSS
攻击
开发时遇到多个 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)
})
CORS
CORS
需要浏览器和后端同时支持, IE8
和 IE9
需要通过 XDomainRequest
实现。
浏览器会自动进行 CORS
通信,实现 CORS
通信的关键是后端。只要后端实现了 CORS
,就实现了跨域。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置 CORS
和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
以 Ajax
为例,当满足以下条件时,会触发简单请求
使用下列方法之一:
GET
HEAD
POST
Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload
对象均没有注册任何事件监听器; XMLHttpRequestUpload
对象可以使用 XMLHttpRequest.upload
属性访问。
复杂请求
那么很显然,不符合以上条件的请求就肯定是复杂请求了。
对于复杂请求来说,首先会发起一个预检请求,该请求是 option
方法的,通过该请求来知道服务端是否允许跨域请求。
对于预检请求来说,如果你使用过 Node
来设置 CORS
的话,可能会遇到过这么一个坑。
以下以 express
框架举例:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
)
next()
})
该请求会验证你的 Authorization
字段,没有的话就会报错。
当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。因为预检请求也会进入回调中,也会触发 next
方法,因为预检请求并不包含 Authorization
字段,所以服务端会报错。
想解决这个问题很简单,只需要在回调中过滤 option
方法即可
res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()
document.domain
该方式只能用于二级域名相同的情况下,比如 a.test.com
和 b.test.com
适用于该方式。
只需要给页面添加 document.domain = 'test.com'
表示二级域名都相同就可以实现跨域
postMessage
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
// 发送消息端
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('验证通过')
}
})
websocket
Websocket
是 HTML5
的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。
WebSocket
和 HTTP
都是应用层协议,都基于 TCP
协议。但是 WebSocket
是一种双向通信协议,在建立连接之后,WebSocket
的 server
与 client
都能主动向对方发送或接收数据。同时,WebSocket
在建立连接时需要借助 HTTP
协议,连接建立好了之后 client
与 server
之间的双向通信就与 HTTP
无关了。 原生 WebSocket API
使用起来不太方便,我们使用 Socket.io
,它很好地封装了 webSocket
接口,提供了更简单、灵活的接口,也对不支持 webSocket
的浏览器提供了向下兼容。
我们先来看个例子:本地文件 socket.html
向 localhost:3000
发生数据和接受数据
<!-- socket.html -->
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('码不停指'); // 向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data); // 接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws'); // 记得安装 ws
let wss = new WebSocket.Server({ port:3000 });
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('imondo.cn')
});
})
反向代理
通过一些代理配置实现跨域请求
- nginx
现在生产环境上使用最广泛的就是 nginx
, 反向代理, 请求至真实的后端请求
location /api {
proxy_pass http://www.test.com/api; # 反向代理到后端服务
proxy_redirect off;
}
开发环境下使用 webpack
的 devSerer
配置
// webpack
devServer: {
proxy: {
'/api/*': {
target: 'http://test.com/devops/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
}
}
}
vite
的 server
配置
export default defineConfig({
server: {
proxy: {
// 字符串简写写法
'/foo': 'http://localhost:4567',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
},
// 使用 proxy 实例
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
configure: (proxy, options) => {
// proxy 是 'http-proxy' 的实例
}
},
// Proxying websockets or socket.io
'/socket.io': {
target: 'ws://localhost:3000',
ws: true
}
}
}
})
一些问题
jsonp
没有问题,如果后台不配合怎么办?这种方式可能只会在一些老的工程中会出现。- 用
cors
,但是在联调后台还是不愿意设置,特别要考虑到大公司这种环境。 postmessage
,websocket
这种通信模式根本不适合类假于ajax
这种模拟跨遇方式。- 可能你会想到介入
node
中间件进行一个代理。做一个proxy
的二次请求。如果工程是在一个难以集成环境或者不容易集成的环境的情况下。还有什么方案? - 你可能会写到用
nginx
,但是不代表每个前端都会nginx
- 终级办法用关闭浏览器跨域策略。但是可能面经会问其根本原因。浏览器发送了本质上是返回了。正是因为
options
的预检导致的问题。所以关闭浏览器跨遇策略就是关闭了预检。
参考: