【nodejs】解决跨域问题

JavaScript/前端
304
0
0
2023-01-13
标签   NodeJs

跨域问题

  • 跨域:浏览器同源策略引起的接口调用问题
  • 同源策略: 主机 端口 协议
  • 接口调用: XMLHttpRequestFetch 都遵循同源策略
  • 浏览器:浏览器发现可疑行为,拒绝接收

浏览器限制跨域请求一般有两种方式:

  • 浏览器限制发起跨域请求
  • 跨域请求可以正常发起,但是返回的结果被浏览器拦截了

一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。

为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须先使用 OPTIONS 方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。

把这个选项勾上就可以看到预检请求了,关于预检请求,可以参看下面文章。

img

img

预检请求 https://www.jianshu.com/p/b55086cbd9af

来看看跨域问题是什么样的。

// http.js
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
    const { method, url } = req;
    if (method == 'GET' && url == '/') {
      fs.readFile('./index.html', (err, data) => {
        res.setHeader('Content-Type', 'text/html');
        res.end(data);
      });
    } else if (method == 'GET' && url == '/api/users') {
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify([{ name: 'warbler', age: 23 }]));
    }
  })
  .listen(4000, () => {
    console.log('api listen at ' + 4000);
  });
  
// proxy.js
const express = require('express')
const app = express()
app.use(express.static(__dirname + '/'))
app.listen(3000) 

// 可以同时启用两个服务器
const api = require('./http')
const proxy = require('./proxy')

// index.html
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  (async () => {
    axios.defaults.baseURL = 'http://localhost:4000' 
    const res = await axios.get("/api/users")
    console.log('data', res.data)
    document.writeln(`Response : ${JSON.stringify(res.data)}`)
  })()
</script>

当我们直接访问 http://localhost:4000/ 的时候,是可以正常取到数据的。

img

当我们通过 3000 端口去访问 http://localhost:4000/ 的时候,就会产生跨域错误。

img

通过这里也能看出来是一个跨域错误(CORS error

img

解决跨域问题

响应简单请求

响应简单请求:

动词为 get / post / head

没有自定义请求头

Content-Type 是 application/x-wwwform-urlencodedmultipart/form-datatext/plain 之一 通过添加以下响应头解决:

res.setHeader("Access-Control-Allow-Origin", 'http://localhost:3000')

响应预检请求

该案例中通过添加自定义的 x-token 请求头使请求变为预检 (preflight) 请求。

// index.html
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  (async () => {
    axios.defaults.baseURL = 'http://localhost:4000' 
    const res = await axios.get("/api/users", {
      Headers: {
        "X-Token": "aaabbb"
      }
    })
    console.log('data', res.data)
    document.writeln(`Response : ${JSON.stringify(res.data)}`)
  })()
</script>

响应 preflight 请求,需要响应浏览器发出的 options 请求(预检请求),并根据情况设置响应头。

// http.js 
 else if (method == 'OPTIONS') {
      res.writeHead(200, {
        "Access-Control-Allow-Origin": "http://localhost:3000",
        "Access-Control-Allow-Headers": "X-Token,Content-Type",
        "Access-Control-Allow-Methods": "PUT"
      });
      res.end();
    }

响应 credential 请求

如果要携带 cookie 信息,则请求变为 credential 请求:

// 预检options中和/users接口中均需添加
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 设置cookie
res.setHeader('Set-Cookie', 'cookie1=va222;'
// ajax服务需要设置
axios.defaults.withCredentials = true
// 服务端查看cookie 
console.log('cookie',req.headers.cookie)
// index.html
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
  const { method, url } = req;
  if (method == 'GET' &amp;&amp; url == '/') {
    fs.readFile('./index.html', (err, data) => {
      res.setHeader('Content-Type', 'text/html');
      res.end(data);
    });
  } else if (method == 'GET' &amp;&amp; url == '/api/users') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader("Access-Control-Allow-Origin", 'http://localhost:3000')
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader("Set-Cookie", 'cookie1=123')
    res.end(JSON.stringify([{ name: 'warbler', age: 23 }]));
  } else if (method == 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.writeHead(200, {
      "Access-Control-Allow-Origin": "http://localhost:3000",
      "Access-Control-Allow-Headers": "X-Token,Content-Type",
      "Access-Control-Allow-Methods": "PUT"
    });
    res.end();
  }
})
  .listen(4000, () => {
    console.log('api listen at ' + 4000);
  });
  
// index.html
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
  (async () => {
    axios.defaults.baseURL = 'http://localhost:4000'
    axios.defaults.withCredentials = true 
    const res = await axios.get("/api/users", {
      headers: {
        "X-Token": "aaabbb"
      }
    })
    console.log('data', res.data)
    document.writeln(`Response : ${JSON.stringify(res.data)}`)
  })()
</script>

反向代理

服务端设置请求转发

const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express()

app.use(express.static(__dirname + '/'))
app.use('/api', createProxyMiddleware({
  target: 'http://localhost:4000', changeOrigin: false
}));
app.listen(3000)

webpack devserver

vue.config.js 中配置的请求代理实际上是 webpack devserver

// vue.config.js
module.exports = {
  devServer: {
    disableHostCheck: true,
    compress: true,
    port: 5000,
    proxy: {
      '/api/': {
        target: 'http://localhost:4000',
        changeOrigin: true,
      },
    },
  }

Socket实现一个即时通讯IM

原理:Net 模块提供一个异步 API 能够创建基于流 TCP 服务器,客户端与服务器建立连接后,服务器可以获得一个全双工 Socket 对象,服务器可以保存 Socket 对象列表,在接收某客户端消息时,推送给其他客户端。

// 用于TCP通讯
const net = require("net")
// 创建服务
const chatServer = net.createServer()
// 用户列表
const clientList = []
// 监听连接事件
chatServer.on('connection', client => {
  // client => 流
  client.write("Hello\n")
  // 添加到用户列表
  clientList.push(client)
  client.on('data', data => {
    // data => 二进制通讯  Buffer 
    console.log('🚀🚀~ receive:', data.toString());
    // 广播
    clientList.forEach((cli) => {
      cli.write(data)
    })
  })
})

// 监听端口
chatServer.listen(9000)

// 通过Telnet连接服务器
// telnet localhost 9000