Express CORS 有时有效,有时无效

Posted

技术标签:

【中文标题】Express CORS 有时有效,有时无效【英文标题】:Express CORS sometimes works and sometimes doesn't 【发布时间】:2021-04-27 23:13:43 【问题描述】:

我有一个位于 main-domain.tld/api/ 的 ngnix 上托管的 Express API 和一个位于 sub.main-domain.tld 的管理面板,用于向我的 API 发送请求。

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

我无法理解 2 件事:

首先是我收到 CORS 错误的原因,因为我启用了所有 源自我的 API 源代码。 第二个是为什么我只收到 70% 的 CORS 错误 当我的管理面板发出请求时,如果我的 API 设置错误,这不应该发生,对吧?

我整天都在寻找解决方案并尝试以各种可能的方式创建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);
);

【问题讨论】:

与您的问题无关,此结构错误await Admins.findOne( 'username': user, 'password': pwd , (err, data) =&gt; ...); 您不使用await 并将回调传递给您的数据库。选择一个或另一个。如果您传递回调,则请求不会返回承诺,因此 await 毫无意义(什么都不做)。如果您不传递回调,那么它将返回一个承诺,您实际上可以从await 中获取结果。 【参考方案1】:

您正在发出触发飞行前的跨源请求(这可以从浏览器中记录的内容中看出)。这是额外级别的 COR,可能由任意数量的事情引起,例如自定义标头、超出允许的小集的内容类型等...显然只有您的一些请求触发飞行前,这就是为什么一些他们工作正常,而其他人则不然。您可以阅读有关 simple 和 pre-flighted 请求的信息,看看是什么导致浏览器决定它必须预先发送请求。

如果您查看 Chrome 调试器的网络选项卡,您将能够看到浏览器发出 OPTIONS 请求并可能返回 404 或未返回正确的标头,这就是飞行前请求失败的原因然后浏览器拒绝 CORs 请求。

要允许预先发送的 CORs 请求,您的服务器必须使用 2xx 状态和正确的 CORS 标头来响应 OPTIONS 请求。

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

由于您正在使用 cors 模块来帮助您,您可以使用该模块 here 阅读有关飞行前请求的信息。

【讨论】:

我在代码中添加了app.options('*', cors());,但问题仍然存在。在 Chrome 的网络选项卡中,我看到有 2 个请求:1° link 和 2° link。 Chrome 控制台中的错误仍然完全相同。 @SteveStifler - 你把app.options('*', cors()); 放在你所有的路线之前了吗?我看到 OPTIONS 请求获得了 200 状态代码,但它没有获得 CORS 标头。当此请求失败时,浏览器控制台现在显示什么? 我把app.options('*', cors());放在app.use(cors());下面。这是控制台中的错误:link. @SteveStifler - 请求排序有问题,因为您在对 OPTIONS 请求的响应中没有收到 Access-Control 类型的标头。您的响应标头屏幕截图未显示其中任何一个。 OPTIONS 响应必须是 2xx 状态并且上面有正确的 CORs 响应标头。您缺少标题。如果不查看文件的当前状态,我真的无法做更多事情。 @SteveStifler - 您可以在整个源代码文件的外部链接中向我显示文件的当前状态,也可以将其添加到问题的末尾(不要替换您已有的在问题中 - 应该保留在那里),但您可以使用“编辑”链接在问题末尾添加新修订。【参考方案2】:

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

这是新的工作 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 ^/(.*)$ /$1.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 标头”的说明,至少发送一次正确的 CORs 标头:

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

【讨论】:

以上是关于Express CORS 有时有效,有时无效的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 == 比较两个整数有时有效,有时无效? [复制]

R 不一致:为啥 add=T 有时有效,有时在 plot() 函数中无效?

为啥打印 unsigned char 有时有效,有时无效?在 C 中

CSS 过滤器 - 有时有效,有时无效?

在 viewDidLoad 和 viewDidAppear 之前 performSegueWithIdentifier 有时有效,有时无效

document.write 有时有效,有时无效