跨域处理

241 阅读8分钟

跨域

什么是跨域

跨域是指一个人域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的,我们所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景

什么是同源策略?

所谓同源是指 协议+域名+端口 相同,这是一种约定,他是浏览器最核心也最基本的安全功能,如果缺少同源策略,浏览器很容易受到 XSS、CSFR 等攻击

即使两个人不同的域名指向同一个ip地址,也不算同源 (www.xxx.com、m.xxx.com、blog.xxx.com)

同源策略限制一下几种行为

  • Cookie、LocalStrage 和 IndexDB 无法读取
  • DOM 和 JS对象无法获得
  • AJAX 请求不能发送

JSONP跨域解决方案

因为基本用不到了,不学了hhh

Node.js中间件代理跨域方案

初始化项目并安装 axios 和 express 依赖

npm init --yes
npm i axios express -s

服务器

const fs = require('fs')
const express = require('express')
const { ppid } = require('process')
const app = express()

// 中间件方法
// 设置node_modules为静态资源目录
// 将来在模板中如果使用了src属性 http://localhost:3000/node_modules
app.use(express.static('node_modules'))

// 当请求/路径时返回index.html
app.get('/',(req,res) => {
  fs.readFile('./index.html',(err,data) => {
    if(err) {
      res.statusCode = 500
      res.end('500 Interval Server Error')
    }
    res.statusCode = 200
    res.setHeader('Content-Type', 'text/html')
    res.end(data)
  })
})

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

app.listen(3000)

index.html内

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src='/axios/dist/axios.js'></script>
  <h1>中间件代理跨域</h1>
  <script>
    // 服务器域为localhost:3000,现在向8080发送请求
    axios.defaults.baseURL = 'http://localhost:8080'
    axios.get('/user')
      .then(res => {
        console.log(res);
      })
      .catch(err => {
        console.log(err);
      }) 
  </script>
</body>
</html>

现在启动服务器

nodemon server.js

访问域名localhost:3000,报错请求失败,因为找不到8080

image-20211003200638131

这时因为在前端请求这个接口的时候,可能这个接口会和后端接口不一样,就会导致非同源的状况,为了解决这个问题,可以做一个中间件服务器,设置代理服务器允许跨域的访问这个服务,将每一次请求都转向 localhost:3000 端口,这样在axios请求的路径都会在当前原有服务器上找对应的路径

接下来安装中间件依赖

npm i http-proxy-middleware -s
const express = require('express')
// ***引入中间件模块
const { createProxyMiddleware } = require('Http-proxy-niddleware')
const app = express()

app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  res.header('Access-Control-Allow-Methord', '*')
  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)

新建一个终端运行代理服务器

nodemon proxyServer.js

这时在地址栏输入 localhost:3000,就会以8080端口的跨域请求,访问代理服务器,代理服务器通过中间件将当前请求转到 localhost:3000 处理这个请求

CORS跨域解决方案

同样是上面的例子,稍微修改一下

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

// 中间件方法
// 设置node_modules为静态资源目录
// 将来在模板中如果使用了src属性 http://localhost:3000/node_modules
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 Server 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)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src='/axios/dist/axios.js'></script>
  <h1>CORS跨域</h1>
  <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>

运行服务器,访问localhost:3000,axios向127.0.0.1:3000发起请求会报错

解决这个问题,只需要在服务器端上方加入这段

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

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

// 中间件方法 

刷新页面,请求成功了

axios.post(url [, data[, config]])

使用说明 · Axios 中文说明 · 看云 (kancloud.cn)

当使用axios发起post请求时,第一个为url路径,第二个为请求体数据,第三个为配置选项

配置选项中有一个属性 withCredentials 表示跨域请求时是否需要使用凭证

withCredentials: false, // 默认的

默认情况下关闭,是不允许携带cookie作为登录凭证的,需要打开

在登录鉴权时还需要传输 Token,也要配置

axios.defaults.baseURL = 'http://127.0.0.1:3000'

axios.post('/login',{
  username: 'Max',
  password: '123'
  },{
    // 登录鉴权需要携带 token
    headers:{
      'Authorization': 'is token'
    },
    // 表示跨域请求时是否需要使用凭证 允许携带cookie
    withCredentials: true
}).then(res => {
    console.log(res);
  }).catch(err => {
    console.log(err);
	}) 

默认后端也是不允许携带 token 和 cookie 的,需要各自添加一行代码

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

在请求标头可以看到token令牌,一般这个令牌是前端发起请求,后端返回的一串随机码,服务器会接收并验证这个 token 直接放行通过,完成登录操作

以上就是 CORS 解决跨域的办法,但是还是需要配置

CORS插件

有一个CORS插件来帮我们完成这些工作

expressjs/cors: Node.js CORS middleware (github.com)

npm i cors -s

然后只需要在服务器内引入模块并使用就可以了

const cors = require('cors')
app.use(cors())

就可以替代app.all('*',function(req,res,next){ })

cors() 默认是不支持携带cookie的,需要配置属性

app.use(cors({
  origin: 'http://localhost:3000',
  // 允许证书
  credentials: true,
  // 允许头 授权,这里token的key设为了 Authorization
  // 上面的 X-Token 也要改成 Authorization,请求头数据也要以Authorization作为key读取
  allowedHeaders: 'Content-Type,Authorization'
}))

下面是cors插件的其他配置选项(机翻加部分修正)

  • origin:配置Access-Control-Allow-Origin CORS header。可能的值:

    • Boolean - 如果设置为false,将设置为禁用CORS

    • String - 设置为特定请求源。例如,将其设置为仅来自"http://example.com"的请求

    • RegExp - 设置为常规表达模式,用于测试请求源。如果是匹配,请求源将进行反映。

      例如,该模式将反映来自以"example.com"结尾的原点的任何请求 origin: /example.com$/

    • Array - 设置为一系列有效原产地。每个来源可以是一个或一个。例如,将接受来自"http://example1.com"或"example2.com"子域的任何请求。origin String RegExp : ["example1.com", /.example2.com$/]

    • Function - 设置为实现某些自定义逻辑的功能。该函数将请求源作为第一参数,将回调(称为选项的非功能值)作为第二个参数。

      origin callback(err, origin)

  • methods: 配置 Access-Control-Allow-Methods CORS header。

    允许一个逗号分隔的字符串 'GET,PUT,POST' ,或一个数组 ['GET', 'PUT', 'POST']

  • allowedHeaders: 配置 Access-Control-Allow-Headers CORS header。

    允许一个逗号分隔的字符串 'Content-Type, Authorization' ,或一个数组 ['Content-Type', 'Authorization']。

    如果不指定,则默认反映 Access-Control-Request-Headers header

  • exposedHeaders: 配置 Access-Control-Expose-Headers CORS header。

    允许一个逗号分隔的字符串 'Content-Range,X-Content-Range',或一个数组 ['Content-Range', 'X-Content-Range'] 。如果不指定,则不会暴露自定义头。

  • credentials: 配置 Access-Control-Allow-Credentials CORS header。设置为 true 允许证书

  • maxAge: 配置缓存时间 Access-Control-Max-Age CORS header。设置为整数以传递给header,否则将省略。

  • preflightContinue: 将 CORS 飞行前响应传递给下一个处理程序。

  • optionsSuccessStatus: 提供一个状态代码用于成功的请求, 因为一些旧的浏览器 (IE11, 各种智能电视) 窒息.OPTIONS``204

AXIOS配置

配置全局axios的三个默认值,可以指定将被用在各个请求的配置默认值

axios.defaults.baseURL = 'https://www.example.com'
axios.defaults.header.common['Ahtuorization'] = AUTH_TOKEN
axios.defaults.header.post['Content-Type'] = 'application/x-www-from-urlencoded'

有了中间这条我们就可以不用每个请求,都吧headers写在axios内部了

axios.defaults.baseURL = 'http://127.0.0.1:3000'
axios.defaults.header.common['Ahtuorization'] = 'drytfgh4rt43456tuf2rtjiorw'

axios.post('/login',{
  username: 'Max',
  password: '123'
  },{
    //headers:{
    //  'Authorization': 'is token'
    //},
    withCredentials: true
}).then(res => {
    console.log(res);
  }).catch(err => {
    console.log(err);
	}) 

第三条则是在传递请求体给服务端接收的设置,服务器端通过req.body接收请求体

服务器端会接收到的请求体会是 键值对数据,当你用中间件时,默认得到的会是 undefined,这时需要用以下两种方法解析

  • express.json() — 将数据解析为json
  • express.urlencoded() — 将数据解析为urlencoded

使用方法是在服务器的上方写入

app.use(express.json())

这时通过打印请求体

app.post('/login', (req,res) => {
  console.log(req.body)
  res.json({status: 0, message: '登陆成功'})
})

就会获得请求体数据

{ username: 'Max', password: '123' }

但是,大部分的后端默认的接收格式都是urlencoded,因为这样的数据好统一管理

因为json字符串所管理的数据,有可能是数字、字符串、数组 或 对象,json不易于管理,所以需要解析为 urlencoded

结果则是 username=Max&password='123'

//客户端axios
axios.defaults.header.post['Content-Type'] = 'application/x-www-from-urlencoded'

//客户端引入qs库 (qs是一个url参数转化(parse和stringify)的js库)
<script src='https://cdn.bootcss.com/qs/6.9.1/qs.js'><script>

qs.js - 更好的处理url参数 - 簡書 (jianshu.com)

但是只是这样还不行,服务器端解析的结果会是一个空对象,这需要使用axios的拦截器,用函数对请求体做处理

axios.defaults.baseURL = 'http://127.0.0.1:3000'
axios.defaults.headers.common['Authorization'] = 'drytfgh4rt43456tuf2rtjiorw'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-from-urlencoded'
//-------------------------------------------------------------------------
// 在返送请求前拦截器拦截请求
axios.interceptors.request.use(function (config) {
  // 获取请求体数据
  let data = config.data
  // 调用Qs的stringify方法将请求体json数据格式转换为urlencoded
  data = Qs.stringify(data)
  console.log(data)
  return config
}), function (err) {
  // 对请求错误做些什么
  return Promise.reject(err)
}
//--------------------------------------------------------------------------
axios.post('/login',{
  username: 'Max',
  password: '123'
},{
  withCredentials: true
})
  .then(res => {
    console.log(res);
  }).catch(err => {
    console.log(err);
  }) 

这时在客户端的请求体就被转换为 urlencoded 格式了

服务器端需要加上

app.use(express.urlencoded({ extended: true }))

req.body 并没有获取到 urlencoded,算了跳过...

NginX反向代理

实现原理类似于Node中间件代理,需要搭建一个中专nginx服务器,用于转发请求

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

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

image-20211004185148731