2.跨域

409 阅读4分钟

什么是跨域?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

1. 资源跳转:<a>标签链接、重定向、表单提交
2. 资源嵌入:<link>、<script>、<img>、<frame>等dom标签,样式中的background:url()、@font-face()等文件外链
3. 脚本请求:js发起的ajax请求、dom和js对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1. CookieLocalStorageIndexDB 无法读取
2. DOMJs对象无法获得
3. Ajax请求不能发送

只要端口号之前有一个不一样就属于跨域

出现跨域的标识:

跨域常用解决方案

1. 通过jsonp跨域(只接受get请求)
2. 跨域资源共享(CORS最常用)(在后端操作)
3. nginx代理跨域
4. node.js中间件代理跨域(react项目中 proxy http-proxy-middleware)

通过JSONP跨域

通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP跨域</title>
</head>

<body>
    <h1>JSONP跨域</h1>
    <!-- 当在后端设置静态资源目录后,这边的 src 会自动拼接 http://localhost:3000/node_modules -->
    <script src="/axios/dist/axios.js"></script>
    <script>
        //封装一个 jsonp
        axios.jsonp = url => {
            return new Promise((resolve, reject) => {
                //声明 jsonCallBack,挂载到window上
                window.jsonCallBack = function (result) {
                    //result 就是后端返回的结果
                    resolve(result);
                }

                //动态的创建 script 脚本,拼接 url 地址
                // http://127.0.0.1:3000/user?cb=jsonCallBack
                let JSONP = document.createElement('script');
                JSONP.type = 'text/javascript';
                JSONP.src = `${url}?cb=jsonCallBack`;
                //添加脚本
                document.querySelector('head').appendChild(JSONP);
                //不能让它一直显示,不然100个请求就有100个脚本
                setTimeout(() => {
                    document.querySelector('head').removeChild(JSONP);
                }, 1000)
            })
        }
        //axios最新版本已不支持jsonp的方法
        axios.jsonp('http://127.0.0.1:3000/user').then(res => {
            console.log(res);
        }).catch(err => {
            console.log(err);
        })
    </script>
</body>

</html>

server.js

const express = require('express');
const fs = require('fs');
const app = express();
//中间件方法
//设置 node_modules 为静态资源目录,将来在模板中如果使用了 src 属性,
//会自动拼接 http://localhost:3000/node_modules
//否则会报错 axios is not defined
app.use(express.static('node_modules'))

app.get('/', (req, res) => {
    fs.readFile('./index.html', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end('500 Interval Serval Error!');
        }
        res.statusCode = 200;
        //设置响应头
        res.setHeader('Content-Type', 'text/html');
        //返回数据
        res.end(data);
    })
})

//设置回调函数名之后可直接使用 res.jsonp({})
// app.set('jsonp callback name', 'cb');
//或者也可在前端设置好 src=`${url}?callback=jsonCallBack`

app.get('/user', (req, res) => {
    //获取查询参数
    const cb = req.query.cb;
    //执行cb,前后端配合,拿到回调函数,执行回调函数
    res.end(`${cb}(${JSON.stringify({name:'holo'})})`);

    //设置回调函数名后,以下代码相当于上面两句代码
    /* res.jsonp({
        name: 'holo'
    }); */
})

app.listen(3000);

通过Node.js中间件代理跨域

前端发起axios请求的时候先经过代理中间件,再把所有的请求转向自己的服务器

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>中间件代理跨域</h1>
    <!-- 当在后端设置静态资源目录后,这边的 src 会自动拼接 http://localhost:3000/node_modules -->
    <script src="/axios/dist/axios.js"></script>
    <script>
        //设置公共的url,所有的请求的url都会拼接
        // axios.defaults.baseURL = 'http://localhost:8080';
        axios.get('http://localhost:8080/user').then(res => {
            console.log(res);
        }).catch(err => {
            console.log(err);
        })
    </script>
</body>

</html>

server.js

const express = require('express');
const fs = require('fs');
const app = express();
//中间件方法
//设置 node_modules 为静态资源目录,将来在模板中如果使用了 src 属性,
//会自动拼接 http://localhost:3000/node_modules
//否则会报错 axios is not defined
app.use(express.static('node_modules'))

app.get('/', (req, res) => {
    fs.readFile('./index.html', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end('500 Interval Serval Error!');
        }
        res.statusCode = 200;
        //设置响应头
        res.setHeader('Content-Type', 'text/html');
        //返回数据
        res.end(data);
    })
})

app.get('/user', (req, res) => {
    res.json({
        name: 'holo'
    });
})

app.listen(3000);

proxyServer.js

const express = require('express');
//加载中间件
const {
    createProxyMiddleware
} = require('http-proxy-middleware');
const app = express();

//代理服务器操作
//设置允许跨域访问该服务.
app.all('*', function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*'); //允许访问所有的服务器
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Content-Type', 'application/json;charset=utf-8');
    //继续执行
    next();
});

//http-proxy-middleware
//设置代理中间件(筛子)每个请求来了之后都会转发到 http://localhost:3000 后端服务器
app.use('/', createProxyMiddleware({
    target: 'http://localhost:3000',
    changeOrigin: true
}));

app.listen(8080);

通过CORS跨域

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>CORS跨域</h1>
    <script src="/axios/dist/axios.js"></script>
    <script>
        axios.defaults.baseURL = 'http://127.0.0.1:3000';
        axios.post('/login').then(res => {
            console.log(res);
        }).catch(err => {
            console.log(err);
        })
    </script>
</body>

</html>

若需要在前端发起请求时携带cookies,需要进行设置

// 表示跨域请求时需要使用凭证 允许携带cookies
withCredentials: true
//允许携带token,同时后端需要设置,允许令牌通过
headers: {
	'X-Token': 'some token'
},

//或通过axios进行统一的配置
axios.defaults.baseURL = 'http://127.0.0.1:3002';
//设置统一请求头
axios.defaults.headers.common['Authorization'] = 'Authorization';
//设置post请求的内容类型
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

同时后端也需要进行设置

//允许携带cookies
res.header('Access-Control-Allow-Credentials', 'true');
//允许令牌通过
res.header('Access-Control-Allow-Headers', 'Content-Type,X-Token');

在后端获取post请求体的数据

//设置对应的返回格式
app.use(express.json()) // for parsing application/json

//格式更方便,可以通过请求拦截器在前端对数据进行拦截,并通过qs.stringify()进行处理
//username=holo&password=123
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded

//获取数据
app.post('/profile', function (req, res, next) {
  console.log(req.body)
  res.json(req.body)
})

请求拦截器

//对数据进行处理
axios.interceptors.request.use(function (config) {
	let data = config.data;
	data = Qs.stringify(data);
	config.data = data;
	// 在发送请求之前做些什么
	return config;
}, function (error) {
	// 对请求错误做些什么
	return Promise.reject(error);
});

server.js

const express = require('express');
const fs = require('fs');
const app = express();
//安装 cors
const cors = require('cors');
//设置 cors 中间件 允许跨域访问,相当于下面一段
app.use(cors());

//设置允许跨域访问该服务.
/* app.all('*', function (req, res, next) {
    /// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Content-Type', 'application/json;charset=utf-8');
    next();
}); */

//中间件方法
//设置 node_modules 为静态资源目录,将来在模板中如果使用了 src 属性,
//会自动拼接 http://localhost:3000/node_modules
//否则会报错 axios is not defined
app.use(express.static('node_modules'))

app.get('/', (req, res) => {
    fs.readFile('./index.html', (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end('500 Interval Serval Error!');
        }
        res.statusCode = 200;
        //设置响应头
        res.setHeader('Content-Type', 'text/html');
        //返回数据
        res.end(data);
    })
})

app.post('/login', (req, res) => {
    res.json({
        status: 0,
        message: '登录成功'
    });
})

//监听端口
app.listen(3000)

在 cors 中同样也可以进行是否允许携带cookies等配置:

必须要设置原始地址origin,同时前后端的响应头中的键名要一致,如下都要为Authorization

app.use(cors({
  origin:'http://localhost:3000', //设置原始地址
  credentials:true, //允许携带cookies
  methods:['GET','POST'], //跨域允许的请求方式
  allowedHeaders:'Content-Type,Authorization' //允许请求头的携带信息
}))

Nginx反向代理

实现原理: 同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。类似于Node.js中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路: 通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookies中domain信息,方便当前域cookies写入,实现跨域登录。

代理服务器,需要做以下几个步骤:

  • 接受客户端请求
  • 将请求转发给服务器
  • 拿到服务器响应数据
  • 将响应转发给客户端
// proxy服务器
server {
    listen       81;
    server_name  www.baidu.com;
    location / {
        proxy_pass   http://www.bbbb.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.bbbb.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}