跨域问题涉及到WEB网页安全性问题,使用不当会造成用户隐私泄露风险,但有时业务上又需要进行跨域请求。如何正确的使用跨域功能,既能满足业务需求,又能够满足安全性要求,显得尤为重要。
1.1.同源策略
- 协议相同
- 域名相同
- 端口相同
同源策略限制内容有:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
1.2.为什么要有跨域限制
Ajax 的同源策略主要是为了防止 CSRF(跨站请求伪造) 攻击,如果没有 AJAX 同源策略,相当危险,我们发起的每一次 HTTP 请求都会带上请求地址对应的 cookie,那么可以进行攻击。
其实,准确的来说,跨域机制是阻止了数据的跨域获取,不是阻止请求发送。
2.浏览器跨域的解决方案
2.1.CORS标准
CORS 是一个 W3C 标准,全称是跨域资源共享(CORSs-origin resource sharing),它允许浏览器向跨源服务器,发出XMLHttpRequest请求。
2.1.1.简单请求
只要同时满足以下两大条件,就属于简单请求
条件1:使用下列方法之一:
- GET
- HEAD
- POST
条件2:Content-Type 的值仅限于下列三者之一:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
2.1.2.复杂请求
不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
我们用 PUT
向后台请求时,属于复杂请求,后台需做如下配置:
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
2.2.JSONP跨域
Script不受浏览器同源策略的影响,所以通过 Script 便签可以进行跨域的请求:
- 首先前端先设置好回调函数,并将其作为 url 的参数。
- 服务端接收到请求后,通过该参数获得回调函数名,并将数据放在参数中将其返回
- 收到结果后因为是 script 标签,所以浏览器会当做是脚本进行运行,从而达到跨域获取数据的目的。
//后端逻辑
var url = require('url')
var http = require('http')
http.createServer(function (req, res) {
var data = {
name: "haha"
};
var callback = url.parse(req.url,true).query.callback;
//url.parse(req.url,true).query即{callback:jsonpCallback} ----------------获取函数名callback
res.writeHead(200)
res.end(`${callback}(${JSON.stringify(data)})`)
}).listen(3000, '127.0.0.1')
//服务端接收到请求后获取到回调函数名callback,将数据放在参数中将其返回,即返回callback({"name" : "haha"}) 函数调用
console.log('监听127.0.0.1:3000端口') //注意这里文字一定要引号包裹不然会报错
//注意终端提示:SyntaxError: Invalid or unexpected token 无效或意外的标记,出现这种情况一般是中文的标点符号或者漏写了符号导致的
<!--前端页面-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP实现跨域</title>
</head>
<body>
<script>
function jsonpCallback(data) {
console.log(JSON.stringfy(data))
} //定义回调函数jsonpCallback,收到服务端返回结果callback({"name" : "haha"}),callback=jsonpCallback
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script> //web页面上调用js文件不收同源策略影响
</body>
</html>
2.3.nginx反向代理
实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
先下载nginx,然后将nginx目录下的nginx.conf修改如下:
// proxy服务器
server {
listen 80;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
最后通过命令行 nginx-s reload
启动nginx
// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');