前端跨域问题

267 阅读10分钟

一、什么是跨域,

理解同源策略是什么

同源策略:

 是一个重要的安全策略,它用于限制一个[origin](https://developer.mozilla.org/zh-CN/docs/Glossary/Origin)的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

知识点:跨域只会出现在浏览器上,小程序和APP开发不会有跨域问题。

二、什么情况下会出现跨域

文档示例:

两个 URL 的 protocolport (en-US) (如果有指定的话) 和 host 都相同的话,则这两个 URL 是_同源_。

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是 80)
http://news.company.com/dir/other.html失败主机不同

浏览器现象:

1323276909-5d9076d7807cb.webp

查看操作:

当浏览器url 是 www. aaa.com, 页面里发起其他域名或ip时,就会出现跨域的问题。 使用F12打开,选择console tab,红色报错 access-control-allow-origin。

本地开发跨域问题

浏览器应用开发

 上面讲述到,浏览器是有跨域的

情景:

通常开发一个前端应用的时候,为了更好的快速开发功能,通常会本地搭建一个本地开发web服务,通过访问localhost:8080或者是127.0.0.1:8080,访问本地的服务静态资源。

而在调用api,用的是www.baidu.com域名时,访问后台数据时,一般会出现跨域的问题。

原因:

浏览器url 输入的是localhost:8080,而通过异步请求访问其他域名的地址,就会出现access-control-allow-orign 的问题。

解决方式:

  1. 浏览器插件方式解决(Firefox、Chrome 和 Opera 浏览器)
  2. 通过本地服务器,正向的代理。(其实就是,当你通过浏览器访问静态资源的时候,访问的是本地服务器,调用异步请求的时候,也是访问本地服务器里;从而可以利用这一点。)

实战

第一种:浏览器插件方式解决

  1. 安装插件 Allow CORS: Access-Control-Allow-origin 插件

    Allow CORS: Access-Control-Allow-origin 是一个轻量级插件,可让您在使用跨域 ajax 请求时轻松允许 CORS

    只需激活插件并执行请求。现代浏览器默认阻止 CORS 或跨源资源共享(在 javascript API 中)。安装此插件将允许您取消阻止此功能。

    使用 Allow CORS 的 4 个理由:Access-Control-Allow-Origin

    • 具有带有 ON|OFF 开关的工具栏弹出窗口。
    • 当插件处于活动或非活动状态时,工具栏按钮会更改图标。
    • 具有最小 RAM 和 CPU 使用率的轻量级插件。
    • 适用于所有操作系统上的 Firefox、Chrome 和 Opera 浏览器。

    操作使用:

    如何下载 Allow CORS: Access-Control-Allow-Origin

    1. Chrome Web Store 下载此扩展程序
    2. Firefox 浏览器中打开 Firefox 下载链接(如果有),然后右键单击 - 添加到 Firefox - 按钮并选择 - 将链接另存为... - 物品。选择您机器上的目标文件夹,然后以 XPI 格式保存文件。然后,您可以将 XPI 格式重命名为 RAR 或 ZIP 文件。

    google chrome 启用插件 Image.png

Image 2.png 解除浏览器跨域限制: 点击左下角的 toggle 按钮,操作后会变亮

Image 3.png

Image 4.png

结果:

  启动后,就解决跨域的问题啦,可以随意访问。

第二种:通过本地服务器,正向的代理。

前端应用的代理方式

1. 基于express 搭建web应用的开发框架

本地服务

const express = require('express')
const app = express()

//如果它在最前面,后面的/开头的都会被拦截
app.get('/', (req, res) => res.send('Hello World!'))

app.use(express.static('public'));//静态资源
app.use('/dist', express.static(path.join(__dirname, 'public')));//静态资源

//404
app.use('/test', function (req, res, next) {
  res.status(404).send("Sorry can't find that!");
});

app.use(function (req, res, next) {
  //TODO 中间件,每个请求都会经过
  next();
});

app.use(function (err, req, res, next) {
  //TODO 失败中间件,请求错误后都会经过
  console.error(err.stack);
  res.status(500).send('Something broke!');
  next();
});

app.listen(4000, () => console.log('Example app listening on port 4000!'))


与request配合使用

这样就将其它服务器的请求代理过来了

const request = require('request');
app.use('/base/', function (req, res) {
  let url = 'http://localhost:3000/base' + req.url;
  req.pipe(request(url)).pipe(res);
});


使用http-proxy-middleware

const http_proxy = require('http-proxy-middleware');
const proxy = {
 '/tarsier-dcv/': {
  target: 'http://192.168.1.190:1661'
 },
 '/base/': {
  target: 'http://localhost:8088',
  pathRewrite: {'^/base': '/debug/base'}
 }
};
 
for (let key in proxy) {
 app.use(key, http_proxy(proxy[key]));
}


监听本地文件变化

使用nodemon插件。

--watch test指监听根目录下test文件夹的所有文件,有变化就会重启服务。

"scripts": {
 "server": "nodemon --watch build --watch test src/server.js"
}

2. 基于koa 搭建web应用的开发框架

CORS 跨域

  这种方案是目前最容易理解也是醉强大的方案,其重点在于后端服务器的实现。跨域资源共享的原理就是相当于将请求资源的域设置到一个白名单中,只要是白名单中的域,就可以访问到当前域的资源。

当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

学习access-control-allow-origin

Access-Control-Allow-Origin 就相当于一个白名单,后台需要设置这个白名单,请求的域在这个白名单中才能获得相应的资源。

在 Koa 中,我们可以通过上下文对象来设置 Access-Control-Allow-Origin,同时由于 Koa 的中间件机制,可以将跨域的处理逻辑放在最前面,当进行路由匹配前,检查请求资源的域是否在白名单中,总体的设置如下:

const Koa = require('koa')
const Router = require('koa-router')
const bodypaser = require('koa-bodyparser')
const static = require('koa-static')
const path = require('path')

const app = new Koa()
const router = new Router()
app.use(bodypaser())

// 处理跨域
app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", "*")
  await next()
})

// 请求数据的接口
router.get('/getData', async (ctx, next) => {
  ctx.body = {
    msg: "ok",
    data: {
      name: "huahua",
      age: "18"
    }
  }
})
app.use(router.routes())

app.listen(3000)

学习设置请求头

app.use(async (ctx, next) => {
  // 允许来自所有域名请求
  ctx.set("Access-Control-Allow-Origin", "*");

  // 设置所允许的HTTP请求方法
  ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");

  // 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段.
  ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");

  // 服务器收到请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

  // Content-Type表示具体请求中的媒体类型信息
  ctx.set("Content-Type", "application/json;charset=utf-8");

  // 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。
  // 当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*";
  ctx.set("Access-Control-Allow-Credentials", true);

  // 该字段可选,用来指定本次预检请求的有效期,单位为秒。
  // 当请求方法是PUT或DELETE等特殊方法或者Content-Type字段的类型是application/json时,服务器会提前发送一次请求进行验证
  // 下面的的设置只本次验证的有效时间,即在该时间段内服务端可以不用进行验证
  ctx.set("Access-Control-Max-Age", 300);

  /*
  CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
      Cache-Control、
      Content-Language、
      Content-Type、
      Expires、
      Last-Modified、
      Pragma。
  */
  // 需要获取其他字段时,使用Access-Control-Expose-Headers,
  // getResponseHeader('myData')可以返回我们所需的值
  //https://www.rails365.net/articles/cors-jin-jie-expose-headers-wu
  ctx.set("Access-Control-Expose-Headers", "myData");

  await next();
})

Koa2-cors 中间件的使用

将上段内容设置请求头的方式,简化可以使用koa2-cors插件

具体代码:

const Koa = require('koa');
const app = new Koa();
// 引入插件
const cors = require('koa2-cors');
// 配置插件
app.use(cors({
  // 任何地址都可以访问
  origin:"*",
  // 指定地址才可以访问
  // origin: 'http://localhost:8080',
  maxAge: 2592000,
  // 必要配置
  credentials: true
}})

POSTMESSAGE 实现页面与页面之间的跨域

window.postMessage()是h5的一个新API,用来实现跨源通信,window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个 MessageEvent 消息。

otherWindow.postMessage(message, targetOrigin, [transfer])

三个参数的作用

otherWindow: 接受消息的窗口(window)。

message:需要发送的消息。

targetOrigin:如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;例如,当用postMessage传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的origin属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。

//x.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <iframe src="http://y.localhost.org:6789/y.html" id="iframe"></iframe>
    <script>
       const iframe=document.getElementById("iframe")
       iframe.onload=function(){
       	 //向y.html这个窗口发送消息
         iframe.contentWindow.postMessage("我来啦","http://y.localhost.org:6789")
       }
       //监听y.html窗口发来的消息
        window.addEventListener("message", function(e){
            console.log(e.data)//{msg: "helloword"}
        }, false);
    </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
   <h1>xxxxxxx</h1>
    <script>
    	 //监听x.html窗口发来的消息
          window.addEventListener("message",function(e){
          	//并页面h1把值换成传来的数据
            document.querySelector("h1").innerText=e.data;
            //给y.html回应消息
            parent.postMessage({"msg":"helloword"},event.origin);
          })
    </script>
</body>
</html>

3. 基于webpack 打包器, webpack-dev-server 作为开发服务器

问题:

 在开发阶段,目前遇到的问题时打包,运行,调试过程过于繁琐。
  1. 编写代码
  2. 控制台运行命令完成打包
  3. 打开页面查看效果
  4. 继续编写代码,回到2步骤

利用 webpack-dev-server

  1. 安装
  2. 使用 wepack-dev-server

安装

npm install webpack@4.46.0 webpack-dev-server@4.9.3 html-webpack-plugin@4.5.1 -D

实现webpack-dev-server 作为开发服务器

webpack.dev.config.js

const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpackConfig = {
    entry: "./index.js",
    mode:'development',
    output:{
        filename: 'index_bundle.js',
        path: __dirname + '/dist',
      },
    devServer: {
    static: __dirname + './dist',
    },
    plugins: [new HtmlWebpackPlugin()],
}
const compiler = Webpack(webpackConfig);
const devServerOptions = { ...webpackConfig.devServer, open: true };
const server = new WebpackDevServer(devServerOptions, compiler);

const runServer = async () => {
  console.log('Starting server...');
  await server.start();
};

runServer();

webpack-dev-server 原理

webpack-dev-server在内部使用Express搭建搭建了一个小型Node服务来接收处理后的文件,那是什么程序传递文件的呢? 就是webpack-dev-middleware

webpack-dev-middleware

定义:

`webpack-dev-middleware` 是一个容器(`wrapper`),它可以把 `webpack` 处理后的文件传递给一个服务器(`server`)。

`webpack-dev-server` 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。

demo实战:

使用Express,搭配`webpack-dev-middleware`来实现**文件更新功能**
  1. ├── dist
  2. │ └── index.html
  3. ├── package.json
  4. ├── src
  5. │   └── index.js
  6. └── webpack.dev.config.js

下载:

npm i webpack webpack-cli webpack-dev-middleware express --save-dev

webpack.dev.config.js

const path = require('path');
 
module.exports = {
  entry: './src/index.js',
  mode: 'development',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'
  }
};

server.js

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const path = require('path');
 
const app = express();
const config = require('./webpack.dev.config.js');
const compiler = webpack(config);
// const DIST_DIR = path.join(__dirname, "dist")
 
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));
// 使用静态资源目录,才能访问到/dist/idndex.html
app.use(express.static(config.output.path))
 
// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

/src/index.js

'use strict'
document.write('hello world~')

/src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack-dev-middleware</title>
  <script src="./index.js"></script>
</head>
<body>
</body>
</html>
复制代码

package.json // 增加命令 node server.js

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
 	 "server": "node server.js"
}
直接访问`http://localhost:3000/`

代码编写完成就实现了,热更新,本地调试,基本的打包功能。

怎么处理跨域

http-proxy-middleware使用

定义:

是一个代理中间件,用于转发请求,将客户端发送的请求数据转发到目标服务器,再将响应输送到客户端。

实现方式:

const { proxy } = require("http-proxy-middleware");

if (process.env.NODE_ENV === "development") {
  module.exports = function (app) {
    // Dev server
    app.use(proxy('/api', {target: "http://abc:8080"}));
    // third api (report)
    app.use(proxy('/reports', {target: "http://xyz:9000"}));
    // mock servers
    app.use(proxy(["/user", "/myclients"], { target: "http://localhost:3005" }));
  };
}