深耕跨域问题 (CORS)

·  阅读 322

来自后端同学的灵魂拷问:

  • 我postman可以正常获取数据,你那边为啥不行?
  • 你说跨域是浏览器为安全限制的,那浏览器不是属于前端操作?
  • 浏览器报出的cors异常不是我抛的,是你自己抛出来的异常?
  • ...

CORS

“cors” 跨源资源共享(Cross-Origin Resource Sharing)。origin(源)—— 域(domain)/端口(port)/协议(protocol)的组合;跨源请求就是两个不同的源产生的请求(域名、端口、协议任意一个不同就是不同的源)。下面复现一个跨域问题并解决:

静态文件服务

// htmlServer.js
import express from 'express'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const app = express();
app.set('views', path.join(__dirname));
app.set('view engine', 'ejs');

app.get('/', (req, res) => res.render('index'))

app.listen(11310, () => console.log('11310 port start for html server'))
复制代码
// index.ejs
<!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>
  hohoho
</body>

</html>
复制代码

image.png

api服务

// apiServer.js
import express from 'express'
const app = express();
const router = express.Router()

router.get('/userinfo', (req, res) => {
  console.log(req.headers.referer, '有一个请求到来')
  res.json({ name: 'zhangsan', age: 18 })
})

app.use('/api', router)

app.listen(11311, () => console.log('11311 port start for api server'))
复制代码

通过postman或浏览器直接输入api地址可以拿到json数据 image.png

跨源问题

在postman中发送请求或者在电脑上的静态html中发送请求不会出现跨源请求,是因为发送方没有源,并不是以源到源的方式交互的。如果静态文件被挂在了代理服务器上之后,再从这个静态文件脚本中发出的资源请求那就可能产生跨源问题。当在上述静态文件服务中的ejs中通过fetch请求访问 11311 端口的 /api/userinfo 资源将出现跨源问题

// index.ejs 部分代码
const getUserInfo = async () => {
  try {
    const res = await fetch('http://localhost:11311/api/userinfo');
    console.log(res);
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.log(`出错了:${err}`)
  }
}
getUserInfo();
复制代码

页面控制台输出:

image.png

接口服务控制台输出:

image.png

虽然出现跨源,但是此时在api服务log中可以看到“有一个请求到来”的打印信息,说明后端服务是收到了请求并做出了响应,但是js脚本并没有拿到后端给的响应结果,是因为服务端没有指定允许的域而被浏览器的安全限制拦截掉了。

image.png

当api服务设置好Access-Control-Allow-Origin后可以正常拿到返回的json数据

// apiServer.js
import express from 'express'
const app = express();
const router = express.Router()

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:11310')
  next()
})

router.get('/userinfo', (req, res) => {
  console.log(req.headers.referer, '有一个请求到来')
  res.json({ name: 'zhangsan', age: 18 })
})

app.use('/api', router)

app.listen(11311, () => console.log('11311 port start for api server'))
复制代码

image.png

看似正常,当使用DELETE、PUT、PATCH等请求时,又会出现跨源问题

// apiServer.js
import express from 'express'
const app = express();
const router = express.Router()

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:11310')
  next()
})
app.use(express.json())

router.get('/userinfo', (req, res) => {
  console.log(req.headers.referer, '有一个请求到来')
  res.json({ name: 'zhangsan', age: 18 })
})

router.delete('/user', (req, res) => {
  console.log(req.headers.referer, '有一个delete请求到来')
  res.json({ code: 0, message: 'ok', data: req.body })
})

app.use('/api', router)

app.listen(11311, () => console.log('11311 port start for api server'))
复制代码
// index.ejs 部分代码
const getUserInfo = async () => {
  try {
    const res = await fetch('http://localhost:11311/api/user', {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ id: 'test' }),
    });
    console.log(res);
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.log(`出错了:${err}`)
  }
}
deleteUser();
复制代码

image.png

跨源问题再次出现,且这个时候api服务中的响应函数中不能收到该请求(请求还没到达该函数),浏览器控制台请求中出现一个同资源名的OPTIONS类型的请求,这同样是浏览器安全限制导致

image.png

任何非简单请求 —— 浏览器不会立即发出实际请求。会先发送“预检(preflight)”请求来请求许可;预检请求使用 OPTIONS 方法。如果服务器同意处理请求,那么它会进行响应,此响应的状态码应该为 200,没有 body,具有 header:

  • Access-Control-Allow-Origin 必须为 * 或进行具体请求的源(例如 http://localhost:11310)才能允许此请求。
  • Access-Control-Allow-Methods 必须具有允许的方法。
  • Access-Control-Allow-Headers 必须具有一个允许的 header 列表。
  • 另外,header Access-Control-Max-Age 可以指定缓存此权限的秒数。让浏览器不必为满足给定权限的后续请求发送预检。

当api服务设置好相关响应头后可以正常拿到返回的json数据

// apiServer.js 部分代码
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:11310')
  res.setHeader('Access-Control-Allow-Methods', 'PUT,PATCH,DELETE')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
  next()
})
复制代码

image.png 默认情况下,由 JavaScript 代码发起的跨源请求不会带给服务器任何凭据(cookies 或者 HTTP 认证(HTTP authentication))。要在 fetch 中发送凭据,我们需要添加 credentials: "include" 选项,像这样:

fetch('http://localhost:11311/userinfo', {
  credentials: "include"
});
复制代码

如果服务器同意接受 带有凭据 的请求,服务器还应该在响应中添加 header Access-Control-Allow-Credentials: true

注意:对于具有凭据的请求,禁止 Access-Control-Allow-Origin 使用星号 *,它必须有一个确切的源。这是一项安全措施,以确保服务器真的知道它信任的请求者的是谁。

简单请求

上面提到“非简单请求”,简单请求是满足以下两个条件的请求:

  1. 简单的方法:GET,POST 或 HEAD
  2. 简单的 header —— 仅允许自定义下列 header:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type 的值为 application/x-www-form-urlencodedmultipart/form-data 或 text/plain

除简单请求以外任何其他请求都被认为是“非简单请求”。例如:

const res = await fetch('http://localhost:11311/userinfo', { 
    method: 'PATCH', 
    headers: { 
        'Content-Type': 'application/json',
        'API-Key': 'secret',
    }
});
复制代码

这里有三个理由解释为什么它不是一个简单请求(任意一个就够了):

  • 方法为 PATCH
  • Content-Type 不是这三个中之一:application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 有自定义header API-Key

代理解决跨域问题

除了后端api服务加响应头的方式,也可以通过代理服务的方式来解决跨域问题,比如nginx,或者就使用上述静态文件服务做代理:

// htmlServer.js
import express from 'express'
import { request } from 'http'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const app = express();
app.set('views', path.join(__dirname));
app.set('view engine', 'ejs');

app.get('/', (req, res) => res.render('index'))
app.use(express.json())
app.use('/api/*', (req, res) => {
  const postData = JSON.stringify(req.body)
  const reqProxy = request({
    hostname: 'localhost',
    port: 11311,
    path: req.originalUrl,
    method: req.method,
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData)
    }
  }, (resProxy) => {
    resProxy.pipe(res)
  })
  if (postData.length > 0) {
    reqProxy.write(postData)
  }
  reqProxy.end();
})

app.listen(11310, () => console.log('11310 port start for html server'))
复制代码
// index.ejs
<!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">
  <link rel="shortcut icon" href="" type="image/x-icon" />
  <title>Document</title>
</head>

<body>
  hohoho
</body>
<script>
  const getUserInfo = async () => {
    try {
      const res = await fetch('/api/userinfo');
      console.log(res);
      const data = await res.json();
      console.log(data);
    } catch (err) {
      console.log(`出错了:${err}`)
    }
  }
  const deleteUser = async () => {
    try {
      const res = await fetch('/api/user', {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ id: 'test' }),
      });
      console.log(res);
      const data = await res.json();
      console.log(data);
    } catch (err) {
      console.log(`出错了:${err}`)
    }
  }
  getUserInfo();
  deleteUser();
</script>

</html>
复制代码

image.png 去掉api服务中的header后,通过简单代理服务器的实现同样解决了跨源问题,事实上代理需要做得完美需要考虑很多的东西,可以使用现成的代理服务器或者使用一些中间件来搭建代理服务。

其他方式解决跨域问题

除了后端通过改变响应头和添加代理服务器以外还有很多方式解决跨源问题:如JSONP、websocket、二级域名、postMessage等

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改