跨域
涉及面试题:什么是跨域?为什么浏览器要使用同源策略?你有几种方式可以 解决跨域问题?了解预检请求嘛?
- 因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有⼀个不同 就是跨域,
Ajax
请求会失败。 - 那么是出于什么安全考虑才会引⼊这种机制呢? 其实主要是用来防止
CSRF
攻击的。简单点说,CSRF
攻击是利用用户的登录态发起恶意请求。 - 也就是说,没有同源策略的情况下, A 网站可以被任意其他来源的
Ajax
访问到内容。
如果你当前 A 网站还存在登录态,那么对方就可以通过Ajax
获得你的任何信息。当然 跨域并不能完全阻止CSRF
。
然后我们来考虑⼀个问题,请求跨域了,那么请求到底发出去没有?
请求必然 是发出去了,但是浏览器拦截了响应。
你可能会疑问明明通过表单的方式可以 发起跨域请求,为什么 Ajax 就不会。
因为归根结底,跨域是为了阻止用户读取到另⼀个域名下的内容, Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。
但是表单并不会获取新的内容,所以可以发起跨域请求。同时也 说明了跨域并不能完全阻止 CSRF ,因为请求毕竟是发出去了。
1. JSONP
JSONP 的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。通过 <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)
})
2. CORS
- CORS 需要浏览器和后端同时⽀持。 IE 8 和 9 需要通过 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'
)
next()
})
- 该请求会验证你的 Authorization 字段,没有的话就会报错。
- 当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。
因为预检请求也会进入回调中,也会触发 next 方法,因为预检请求并不包含 Authorization 字段,所以服务端会报错。
想解决这个问题很简单,只需要在回调中过滤option
方法即可。
res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()
3. document.domain
- 该方式只能用于主域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
- 只需要给页面添加
document.domain = 'test.com'
表示主域名都相同就可以实现跨域。
4. 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('验证通过')
}
})