本文已参与「新人创作礼」活动,一起开启掘金创作之路。
引言
同源策略是浏览器非常重要的安全策略,但在保证安全的同时也带来了许多不便。如果两个页面的协议,域名,端口都相同,则两个页面具有相同的源,否则就是非同源。不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。这是一个用于隔离潜在恶意文件的重要安全机制。我们要解决的就是跨域问题,跨域是因为同源策略而引发的问题。
HTML中也有不受同源策略限制的标签,比如页面中的链接<a>,重定向以及表单提交是不会受到同源策略限制的。跨域资源的引入其实是可以的,只是js不能读写加载的内容。如嵌入到页面中的<script src="..."></script>,<img>,<link>,<iframe>等。
受限制的主要是Cookie、LocalStorage 和 IndexDB ,这些是无法读取的;DOM和JS对象无法获得;AJAX 请求不能发送。我们就来讲解跨域的一些解决方案
jsonp回调方案
jsonp实现跨域的核心原理就是:目标页面回调本地页面的方法,并带入参数。 服务器端获取客户端发送过来的query参数,其中参数有回调函数的名字,得到的数据,拼接出一个函数调用的字符串,最后把上一步拼接得到的字符串,响应给客户端的 <script> 标签进行解析执行。
通过script标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。代码实现如下:
<script>
function getData(data){
console.log(data)
}
</script>
<script src="http://127.0.0.1:3000/web?cb=getData"></script>
上面代码,其实就是利用script标签不受同源策略限制,利用src属性来发起请求。换句话说:客户端使用script标签代替XHR( XMLHttpRequest)发起跨域请求。
我们再看下后端的代码,注意:携带参数必须是字符串。
const express=require('express')
const router=express.Router()
router.get('/web',(req,res)=>{
let {cb}=req.query
console.log(req.query)
var data = {
name: 'xtt',
age: 18,
gender:'女孩子'
}
// 携带参数必须是字符串
res.send(`${cb}(${JSON.stringify(data)})`)
router.get('/que',(req,res)=>{
res.send(`${req.query.cb}('dd')`)
})
})
module.exports=router
上述代码,第三行就是解析路径"/web",最后把要返回的数据data封装成json格式,再路由回去。
因为是用script标签发送请求,这也就导致了jsonp方法的不足,那就是只能发起get请求。
上面是原生js实现的,再来看下使用jquery Ajax来实现:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.js"></script>
<script>
let url = 'http://127.0.0.1:3000/que?cb=getData'
$.ajax({
method: 'GET',
url,
dataType: 'jsonp',
success: (res) => {
console.log(res)
}
})
</script>
上面代码是以jquery来发起jsonp请求。现在vue框架很火,我们也可也用 Vue 的axios实现:
handleCallback({"success": true, "user": "admin"})
this.$http = axios;
this.$http.jsonp('http://127.0.0.1:3000/que?cb=getData', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
注意:以上方法发送jsonp请求表面上看起来好像和普通得Ajax请求没有区别,但本质上有很大区别。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加script标签来调用服务器提供的js脚本。
CORS方法
CORS(Cross-origin resource sharing)中文全称"跨域资源共享",是一个W3C标准。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。CORS需要浏览器和服务器同时支持。 目前,所有主流浏览器都支持该功能,IE10以下不支持;浏览器将CORS跨域请求分为:简单请求、非简单请求。我们先说简单请求:
浏览器在发送跨域请求的时候,会先判断下是简单请求还是非简单请求,如果是简单请求,就先执行服务端程序,然后浏览器才会判断是否跨域。同时满足以下的两个条件,就属于简单请求。浏览器对这两种的处理,是不一样的。
- 请求方式:get/post/head其中一种。
- 请求头设置: Accept、 Accept-Language、 Content-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain( 只限于三个值中的一个)。
对于这样的简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
发起请求 ---> 自动在头信息之中,添加一个Origin字段。---> 服务器判断此次请求Origin源
在第二步,添加Origin字段,大致如下:
GET /cors HTTP/1.1
Origin: http://127.0.0.1:8080
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
这个Origin字段就是说明本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
在第三步,服务器判断origin源,有三个与 CORS 请求相关的字段,都以Access-Control-开头。如下:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
如果浏览器发现回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被请求的异常回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。如果不在许可范围内:服务器会返回一个正常的 HTTP 回应。如果在许可范围内:服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin字段是必填的,它的值要么是请求时Origin字段的值,要么是一个星号* ,表示接受任意域名的请求。Access-Control-Allow-Credentials是可选的,是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中(为了降低 CSRF 攻击的风险)。设为true,即表示服务器明确许可,浏览器可以把 Cookie 包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送 Cookie,不发送该字段即可。
至于Access-Control-Expose-Headers字段,也是可选字段。CORS 请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个服务器返回的基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
我们接着讲非简单请求。对服务器提出特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量DELETE和PUT请求,这些传统的表单不可能跨域发出的请求。
举个例子来说,自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 HTTP 头信息:
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
服务器收到“预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。其中Access-Control-Request-Method字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是PUT。Access-Control-Request-Headers字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。
我们看下CORS跨域的前端如何编写:
let xhr;
try {
xhr=new XMLHttpRequest();
} catch (error) {
xhr=new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.open('post','http://localhost:3000/login',true);
xhr.setRequestHeader('content-type','application/x-www-form-urlencoded');
xhr.send('name=111&age=12');
xhr.onreadystatechange=function(){
if(xhr.readyState==4){
let reg=/^2\d{2}/
if(reg.test(xhr.status)){
console.log(JSON.parse(xhr.response))
}
}
}
后端我们使用nodejs来接收,在Express中通过第3方中间件来完成cors跨域解决, 在路由之前调用 app.use(cors()) 配置中间件。
const express=require('express')
const cors=require('cors')
const app=express()
app.listen(3000)
const allowHosts=[
'http://localhost:5000',
'http://localhost:2000'
]
app.use(cors())
app.use((req,res,next)=>{
let hst =req.header.origin
if(allowHosts.includes(hst)){
next()
}else{
return res.send({
code:404,
msg:'地址不对'
})
}
})
app.get('/login',(req,res)=>{
res.send('登陆')
})
注意,需要提前运行 npm install cors 安装中间件,再使用 const cors = require('cors') 导入中间件。