跨域问题
1. 前言
前端道友们在进行前端开发的过程中肯定都遇到过跨域问题,最常见的是不是都是请求后端道友来处理跨域问题呢?反正之前我确实是这么做的,今天想彻底吃透一下关于浏览器跨域的问题,所以写了这篇文章,如有不足之处希望大家能够指正,相互帮助,共同进步。
2. 跨域问题产生的原因
跨域问题其实可以说是浏览器的跨域问题,为什么前面多加了一个“浏览器的”形容词呢?是因为跨域问题只会在浏览器中出现,主要是浏览器的一个安全策略——同源策略导致的。
说到了同源策略,不了解一下什么是同源策略好像说不过去,那就让我们来了解一下什么是同源策略吧!
2.1 同源策略
同源策略是浏览器特有的一种安全策略,简单来说浏览器中访问页面的地址中的协议、域名、端口和浏览器发送的请求地址中的协议、域名和端口完全一致,则说明没有跨域,否则,三者之中有一个不相同,均属于跨域。哪怕不同的域名对应着相同的IP也属于跨域。
2.2 同源策略的限制
- Cookie、LocalStorage、IndexedDB 等存储性内容;
- DOM 节点;
- AJAX 请求发送后,结果被浏览器拦截了;
在跨域的情况下,浏览器依然会发送请求,并且服务器也能返回响应,只不过结果会被浏览器拦截。
你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
2.3 在跨域下依然可以加在资源的标签
<img src=XXX>
<link href=XXX>
<script src=XXX>
2.4 常见的跨域场景
3. 跨域问题的解决方法
3.1 CORS
解决跨域问题最简单的方法就是让后端来允许跨域请求啦。
CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。
同源策略默认禁止跨域请求资源,但是CORS给了web服务器权限,让服务器端可以通过设置允许跨域请求资源。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符(*)则表示所有网站都可以访问资源。
这种方法解决跨域问题,只需要服务端处理就可以了,但是浏览器在发送时会出现两种情况:简单请求和复杂请求;
3.1.1 跨域涉及到的请求头参数
-
Access-Control-Allow-Origin
请求发出去之前,浏览器做了什么?
它会自动在请求头当中,添加一个Origin字段,用来说明请求来自哪个源。服务器拿到请求之后,在回应时对应地添加Access-Control-Allow-Origin字段,如果Origin不在这个字段的范围中,那么浏览器就会将响应拦截。
因此,Access-Control-Allow-Origin字段是服务器用来决定浏览器是否拦截这个响应,这是必需的字段。与此同时,其它一些可选的功能性的字段,用来描述如果不会拦截,这些字段将会发挥各自的作用。 -
Access-Control-Allow-Credentials
这个字段是一个布尔值,表示是否允许发送 Cookie,对于跨域请求,浏览器对这个字段默认值设为 false,而如果需要拿到浏览器的 Cookie,需要添加这个响应头并设为true, 并且在前端也需要设置withCredentials属性:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
- Access-Control-Expose-Headers
这个字段是给 XMLHttpRequest 对象赋能,让它不仅可以拿到基本的 6 个响应头字段(包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma), 还能拿到这个字段声明的响应头字段。比如这样设置:
Access-Control-Expose-Headers: aaa
那么在前端可以通过 XMLHttpRequest.getResponseHeader(‘aaa’) 拿到 aaa 这个字段的值。
- Access-Control-Allow-Headers
Access-Control-Allow-Headers
3.1.2 简单请求
满足一下两个条件的请求都是简单请求:
- 请求方式:GET、POST、HEAD
- 内容类型(content-type):
multipart/form-data
text/plain
application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
3.3.1 复杂请求
不是简单请求的请求都是复杂请求。
浏览器在发送复杂请求之前会先发送一个type:"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') {
// OPTIONS请求不做任何处理
res.end()
}
}
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)
上述代码由 http://localhost:3000/index.html向 http://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。
3.2 jsonp
jsonp是一种比较传统的解决跨域请求的方式,主要是利用了 script 标签没有跨域问题的特性,使浏览器能够跨域请求json数据。jsonp请求需要服务端支持才行。
3.2.1 JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
3.2.2 JSONP优缺点
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
3.2.3 JSONP的实现流程
-
声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
-
创建一个 script 标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
-
服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show(‘我不爱你’)。
-
最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。
// index.html
function jsonp ({url, params, callback}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function (data){
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback }
// wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({url: 'http://localhost:3000/say', params: { wd: 'Iloveyou'}, callback: 'show'})
.then(data => { console.log(data) })
上面这段代码相当于向 http://localhost:3000/say?wd=Iloveyou&callback=show 这个地址请求数据,然后后台返回 show(‘我不爱你’),最后会运行show()这个函数,打印出’我不爱你’
// server.js
let express = require('express')
let app = express()
app.get('/say', function (req, res) {
let {wd, callback} = req.query
console.log(wd)
// Iloveyou
console.log(callback)
// show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
3.2.4 jQuery的jsonp形式
JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。
$.ajax({
url: "http://crossdomain.com/jsonServerResponse",
dataType: "jsonp",
//可以省略
type: "get",
//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonpCallback: "show",
//->把传递函数名的那个形参callback,可省略
jsonp: "callback",
success: function (data){
console.log(data);
}
});
3.3 nginx
使用nginx解决跨域问题,是一种十分简单的解决方式,只需要修改nginx配置即可,支持所有浏览器,支持session,不需要修改任何代码,也不会影响服务器性能。
下载nginx,修改nginx目录下的nginx.conf文件:
- 正向代理
正向代理帮助客户端访问客户端自己访问不到的服务器,然后将结果返回给客户端。
- 反向代理
反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。
因此,两者的区别就很明显了,正向代理服务器是帮客户端做事情,而反向代理服务器是帮其它的服务器做事情。
3.3.1. react项目打包后使用nginx解决跨域问题
1) 修改接口路径:http://127.0.0.1:9080/pkg/….
2) npm run build 打包项目。
3)将打包后的项目放在nginx目录下的html目录下。
4)配置nginx服务器
server {
listen 9080;
server_name 127.0.0.1;
location / {
root html/build;
index index.html index.htm;
}
location ^~ /pkg/ {
proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
}
}
5)nginx -s reload 启动nginx服务器
6)浏览器输入:http://localhost:9080/ 进入项目
如果项目package.json中配置了homePage属性,要将属性值加入路径中,
例如:homePage: “/web”
览器输入:http://localhost:9080/web/
注释:
a. 配置默认打开文件:
location / {
root html/build;
index index.html index.htm;
}
b. 配置反向代理
location ^~ /pkg/ {
proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
}
3.3.2. react项目在开发模式下解决跨域问题以及cookie问题
1)配置nginx服务器,和上面配置相同,但是要配置请求头
2)修改接口路径:http://127.0.0.1:9080/pkg/….
server {
listen 9080;
server_name 127.0.0.1;
location / {
root html/build;
index index.html index.htm;
}
location ^~ /pkg/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header 'Access-Control-Allow-Credentials' 'true';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3141';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header 'Access-Control-Allow-Credentials' 'true';
}
proxy_pass http://yf027.intdev.hxyd.tech:9080/pkg/;
}
}