Express CORS 有时有效,有时无效

Express CORS sometimes works and sometimes doesn't

我有一个 Express API 托管在位于 main-domain.tld/api/ 的 ngnix 上,还有一个位于 sub.main-domain.tld 的管理面板将请求发送到我的 API.

当我尝试从我的管理面板向我的 API 发送请求时,我收到 CORS 错误,70% 的时间与我请求的路由和使用的方法无关(POST、获取等)。

我不明白两件事:

我整天都在寻找解决方案,并尝试以各种可能的方式创建 corsOptions,但我仍然遇到同样的问题,无论我做什么。

CORS 错误:

API源代码:

import cors from 'cors';
import express from 'express';
import jwt from 'jsonwebtoken';

import { generateToken, getCleanUser } from './utils';

import { Admins, Utenti, Prodotti, Acquisti } from './models';

require('dotenv').config();

const app = express();
const port = process.env.PORT;

const mongoose = require('mongoose');
mongoose.connect('mongodb://domain', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
}).then(db => console.log('Il DB è connesso!'))
  .catch(err => console.log(err));

// CORS
app.use(cors());
// parse application/json
app.use(express.json());
// parse application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));

// Middleware that checks if JWT token exists and verifies it if it does exist.
// In all future routes, this helps to know if the request is authenticated or not.
app.use((req, res, next) => {
  // check header or url parameters or post parameters for token
  let token = req.headers['authorization'];
  if (!token) return next(); //if no token, continue

  token = token.replace('Bearer ', '');
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(401).json({
        error: true,
        message: "Invalid user."
      });
    } else {
      req.user = user; //set the user to req so other routes can use it
      next();
    }
  });
});

// request handlers
app.get('/api', (req, res) => {
  if (!req.user) return res.status(401).json({ success: false, message: 'Invalid user to access it.' });
  res.send('Welcome! - ' + req.user.username);
});


//*================================
//* ADMINS SIGNIN
//*================================
app.post('/api/admins/signin', async (req, res) => {
  try {
    const user = req.body.username;
    const pwd = req.body.password;

    // return 400 status if username/password is not exist
    if (!user || !pwd) {
      return res.status(401).json({
        error: true,
        message: "Username or Password required!"
      });
    }

    await Admins.findOne({ 'username': user, 'password': pwd }, (err, data) => {

      if (err) {
        console.error('DB ERROR  =>  ', err);
      }

      // return 401 status if the credential is not match.
      if (!data) {
        return res.status(401).json({
          error: true,
          message: "Username or Password is Wrong!"
        });
      }

      // generate token
      const token = generateToken(data);
      // get basic user details
      const userObj = getCleanUser(data);
      // return the token along with user details
      return res.json({ user: userObj, token });

    });
  } catch (error) {
    console.log(`ERRORE NELLA POST REQUEST DI ADMIN SIGNIN >> ${error}`);
    return res.status(400);
  }
});

//*================================
//* USERS SIGNIN
//*================================
app.post('/api/users/signin', async (req, res) => {
  try {
    const user = req.body.username;
    const pwd = req.body.password;

    // return 400 status if username/password is not exist
    if (!user || !pwd) {
      return res.status(401).json({
        error: true,
        message: "Username or Password required!"
      });
    }

    await Utenti.findOne({ 'username': user, 'password': pwd }, (err, data) => {

      if (err) {
        console.error('DB ERROR  =>  ', err);
      }

      // return 401 status if the credential is not match.
      if (!data) {
        return res.status(401).json({
          error: true,
          message: "Username or Password is Wrong!"
        });
      }

      // generate token
      const token = generateToken(data);
      // get basic user details
      const userObj = getCleanUser(data);
      // return the token along with user details
      return res.json({ user: userObj, token });

    });
  } catch (error) {
    console.log(`ERRORE NELLA POST REQUEST DI USERS SIGNIN >> ${error}`);
    return res.status(400);
  }
});

//*================================
//* USERS SIGNUP
//*================================
app.post('/api/users/signup', async (req, res) => {
  try {
    // return 400 status if username/password is not exist
    if (!req.body.username || !req.body.password || !req.body.email) {
      return res.status(400).json({
        error: true,
        message: "Every field in the form is required!"
      });
    }

    await Utenti.findOne({ 'username': req.body.username }, async (err, data) => {
      if (err) {
        console.error('DB ERROR  =>  ', err);
      }

      if (data) {
        return res.status(400).json({
          error: true,
          message: "Username already taken!"
        });
      }

      await Utenti.find().sort({ _id: -1 }).exec(async (err, lastUserSignedUp) => {
        if (err) return res.status(401).json({
          error: true,
          message: 'DB Problem... '
        });

        let newUser;

        if (lastUserSignedUp[0]) {
          newUser = new Utenti({
            id: (lastUserSignedUp[0].id + 1),
            username: req.body.username,
            password: req.body.password,
            email: req.body.email
          });
        } else {
          newUser = new Utenti({
            id: 0,
            username: req.body.username,
            password: req.body.password,
            email: req.body.email
          });
        }

        await newUser.save();
        return res.json(newUser);
      });
    });
  } catch (error) {
    console.log(`ERRORE NELLA POST REQUEST DI USERS SIGNUP >> ${error}`);
    return res.status(401)
  }
});

//*================================
//* ADMINS VERIFY THE TOKEN
//*================================
app.get('/api/verifyAdminToken', (req, res) => {
  // check header or url parameters or post parameters for token
  const token = req.body.token || req.query.token;
  if (!token) {
    return res.status(400).json({
      error: true,
      message: "Token is required."
    });
  }

  // check token that was passed by decoding token using secret
  jwt.verify(token, process.env.JWT_SECRET, async (err, user) => {
    try {
      if (err) return res.status(401).json({
        error: true,
        message: "Invalid token."
      });

      await Admins.findOne({ 'username': user.username, 'password': user.password }, (err, data) => {

        if (err) {
          console.error('DB ERROR  =>  ', err);
        }

        // return 401 status if the userId does not match.
        if (user._id !== data._id.toString()) {
          return res.status(401).json({
            error: true,
            message: "Invalid user."
          });
        }
        // get basic user details
        const userObj = getCleanUser(data);
        return res.json({ user: userObj, token });

      });
    } catch (error) {
      console.log(`ERRORE NELLA GET REQUEST DI VERIFY-TOKEN >> ${error}`);
    }
  });
});

//*================================
//* USERS VERIFY THE TOKEN
//*================================
app.get('/api/verifyToken', (req, res) => {
  // check header or url parameters or post parameters for token
  const token = req.body.token || req.query.token;
  if (!token) {
    return res.status(400).json({
      error: true,
      message: "Token is required."
    });
  }

  // check token that was passed by decoding token using secret
  jwt.verify(token, process.env.JWT_SECRET, async (err, user) => {
    try {
      if (err) return res.status(401).json({
        error: true,
        message: "Invalid token."
      });

      await Utenti.findOne({ 'username': user.username, 'password': user.password }, (err, data) => {

        if (err) {
          console.error('DB ERROR  =>  ', err);
        }

        // return 401 status if the userId does not match.
        if (user._id !== data._id.toString()) {
          return res.status(401).json({
            error: true,
            message: "Invalid user."
          });
        }
        // get basic user details
        const userObj = getCleanUser(data);
        return res.json({ user: userObj, token });

      });
    } catch (error) {
      console.log(`ERRORE NELLA GET REQUEST DI VERIFY-TOKEN >> ${error}`);
    }
  });
});

//*================================
//* PRODOTTI
//*================================
app.route('/api/prodotti')
  .get(async (req, res) => {
    try {
      if (req.query.categoria !== undefined) {
        if (req.query.categoria === 'all') {
          await Prodotti.find().exec((err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            return res.json(data);
          })
        } else {
          await Prodotti.find({ 'categoria': req.query.categoria }, (err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            return res.json(data);
          })
        }
      } else {
        if (!req.query.id) {
          await Prodotti.findOne({ 'nome': req.query.nome }, (err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            if (req.query.desc === 'true' && data) {
              return res.json(data.desc);
            } else {
              return res.json(data);
            }
          });
        } else {
          await Prodotti.findOne({ 'id': req.query.id }, (err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            return res.json(data);
          });
        }
      }
    } catch (error) {
      console.log(`ERRORE NELLA GET REQUEST DEI PRODOTTI >> ${error}`);
    }
  })
  .post(async (req, res) => {
    if (req.user) {
      try {
        await Admins.findOne({ 'username': req.user.username, 'password': req.user.password }, async (err, data) => {

          if (err) {
            console.error('DB ERROR  =>  ', err);
          }

          // return 401 status if the credential is not match.
          if (!data) {
            return res.status(401).json({
              error: true,
              message: "Access Denied"
            });
          }

          await Prodotti.find().sort({ _id: -1 }).exec(async (err, lastProdottoAggiunto) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            let nuovoProdotto;

            if (lastProdottoAggiunto[0]) {
              nuovoProdotto = new Prodotti({
                id: (lastProdottoAggiunto[0].id + 1),
                nome: req.body.nome,
                categoria: req.body.categoria,
                prezzo: req.body.prezzo,
                id_api: req.body.id_api,
                desc: req.body.desc
              });
            } else {
              nuovoProdotto = new Prodotti({
                id: 0,
                nome: req.body.nome,
                categoria: req.body.categoria,
                prezzo: req.body.prezzo,
                id_api: req.body.id_api,
                desc: req.body.desc
              });
            }

            await nuovoProdotto.save();
            return res.json(nuovoProdotto);
          });
        });
      } catch (error) {
        console.log(`ERRORE NELLA POST REQUEST DEI PRODOTTI >> ${error}`);
      }
      return res.status(403).json({
        error: true,
        message: 'Access Denied'
      });
    }
  })
  .delete(async (req, res) => {
    try {
      await Prodotti.findOneAndDelete({ 'id': req.body.id }, (err, removed) => {
        if (err) return res.status(401).json({
          error: true,
          message: 'DB Problem... '
        });
        return res.json({ success: 'true' });
      });
    } catch (error) {
      console.log(`ERRORE NELLA POST REQUEST DEI PRODOTTI >> ${error}`);
    }
  });

//*================================
//* ACQUISTI
//*================================
app.route('/api/acquisti')
  .get(async (req, res) => {
    try {
      if (!req.query.id) {
        await Acquisti.find({ 'id': req.query.id }, (err, data) => {
          if (err) return res.status(401).json({
            error: true,
            message: 'DB Problem... '
          });

          return res.json(data);
        });
      }
    } catch (error) {
      console.log(`ERRORE NELLA GET REQUEST DEGLI ACQUISTI >> ${error}`);
    }
  })
  .post(async (req, res) => {
    if (req.user) {
      try {
        const nuovoAcquisto = new Acquisti({
          id: orderId,
          id: req.body.id,
          categoria: req.body.categoria,
          nome: req.body.nome,
          link: req.body.link,
          qty: req.body.qty,
          spesa: req.body.spesa
        });
        await nuovoAcquisto.save();
        return res.json(nuovoAcquisto);

      } catch (error) {
        return res.status(401).json({
          error: true,
          message: `ERRORE NELLA POST REQUEST DEGLI ACQUISTI >> ${error}`
        });
      }
    } else {
      return res.status(403).json({
        error: true,
        message: 'Access Denied'
      });
    }
  });

//*================================
//* UTENTI
//*================================
app.route('/api/utenti')
  .get(async (req, res) => {
    if (req.user) {
      try {
        if (req.query.id) {
          await Utenti.findOne({ 'id': req.query.id }, (err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            return res.json(data);
          });
        } else {
          await Utenti.find().exec((err, data) => {
            if (err) return res.status(401).json({
              error: true,
              message: 'DB Problem... '
            });

            return res.json(data);
          })
        }
      } catch (error) {
        console.log(`ERRORE NELLA GET REQUEST DEGLI UTENTI >> ${error}`);
      }
    } else {
      return res.status(403).send(`
      <div style="text-align: center; font-family: 'Trebuchet MS', sans-serif;">
      </div>
      `);
    }
  })
  .delete(async (req, res) => {
    try {
      await Utenti.findOneAndDelete({ 'id': req.body.id }, (err, removed) => {
        if (err) return res.status(401).json({
          error: true,
          message: 'DB Problem... '
        });
        return res.json({ success: 'true' });
      });
    } catch (error) {
      console.log(`ERRORE NELLA POST REQUEST DEI PRODOTTI >> ${error}`);
    }
  });

app.listen(port, () => {
  console.log('Porta API: ' + port);
});

您正在发出触发 pre-flight 的跨源请求(这可以从浏览器中记录的内容中看出)。这是一个额外的 COR 级别,可能由任何数量的事情引起,例如自定义 headers、content-type 超出一个小的允许集等......显然只有您的一些请求触发 pre-flight 这就是为什么其中一些可以正常工作而另一些则不能。您可以阅读 simple and pre-flighted 个请求,看看是什么导致浏览器决定它必须 pre-flight 个请求。

如果您查看 Chrome 调试器的网络选项卡,您将能够看到浏览器发出 OPTIONS 请求,并且可能返回 404 或未获得正确的 headers 返回这就是 pre-flight 请求失败并且浏览器随后拒绝 CORs 请求的原因。

要允许 pre-flighted CORs 请求,您的服务器必须以 2xx 状态和正确的 CORS headers.

响应 OPTIONS 请求

您将需要一个 app.options(...) 请求处理程序,它允许所有请求通过或仅允许某些请求通过返回正确的 CORS headers 并以 2xx 状态(通常为 204)响应。

由于您使用 cors 模块来帮助您,您可以阅读有关 pre-flight 使用该模块的请求 here

问题是我的 nginx 配置和 Cloudflare 阻止了 COR headers。

这是新的工作 nginx 配置:

server {
        listen 80;
        listen [::]:80;
        
        root /var/www/main-domain.tld;
        index index.html;

        server_name main-domain.tld www.main-domain.tld;
        
        error_page 404 /404.html;
        location = /404.html {
                root /var/www/main-domain.tld;
                internal;
        }

        error_page 400 401 403 503 /custom_50x.html;
        location = /custom_50x.html {
                root /usr/share/nginx/html;
                internal;
        }
        location / {
          if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, HEAD';
            #
            # Custom headers and headers various browsers *should* be OK with but aren't
            #
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            #
            # Tell client that this pre-flight info is valid for 20 days
            #
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
          }
          if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, HEAD';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
          }
          if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, HEAD';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
          }
        
          try_files $uri $uri/ =404;
          if ($request_filename ~* ^.+.html$) {
            break;
          }
          # add .html to URI and serve file, directory, or symlink if it exists
          if (-e $request_filename.html) {
            rewrite ^/(.*)$ /.html last;
            break;
          }
          if (!-e $request_filename){
            rewrite ^(.*)$ /index.html break;
          }
        }
        location /api {
          proxy_redirect off;
          proxy_set_header host $host;
          proxy_set_header X-real-ip $remote_addr;
          proxy_set_header X-forward-for $proxy_add_x_forwarded_for;
          proxy_pass http://localhost:api-port;
        }
        location ~ /\.ht {
                deny all;
        }

}

这是修复 Cloudflare 的方法。按照“添加或更改 CORS headers”的说明进行操作,并至少发送一次正确的 COR headers:

https://support.cloudflare.com/hc/en-us/articles/200308847-Using-cross-origin-resource-sharing-CORS-with-Cloudflare