iT邦幫忙

2023 iThome 鐵人賽

DAY 12
1

https://ithelp.ithome.com.tw/upload/images/20230915/20119486jKfaZIlDX9.png

前言

前一篇我們完成了 Express + Mongoose 的整合,接下來我們要來處理一些環境變數的問題,以及 CORS 的問題。

Cross-origin resource sharing?

Cross-origin resource sharing?看不懂沒關係,我相信你應該知道 CORS 是什麼,假設你今天是一個前端工程師的話,你應該會很常看到這個錯誤訊息:

Access to XMLHttpRequest at 'http://localhost:3000/api/v1/users' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

這個是什麼意思呢?簡單來講就是你目前的網域以及 Prot 與你要存取的網域不同,所以會被瀏覽器擋下來,這個時候你就需要在你的後端加上 CORS 的設定,讓瀏覽器知道你允許哪些網域可以存取你的 API。

那麼這麼又稱之為「跨來源資源共用」,什麼意思呢?簡單來講,當你從 A 網域存取 B 網域的資源時,就稱之為跨來源資源共用。

而這時候為了安全性考量,瀏覽器會擋下來,這時候你就需要在後端加上 CORS 的設定,讓瀏覽器知道你允許哪些網域可以存取你的 API,這樣子就沒有問題了。

(通常最常發生的時機點是 AJAX 的請求。)

那麼我們並不打算深入探討 CORS 的原理,我們基本上只需要知道幾個小重點

  • 當網域不同時,瀏覽器會擋下來
  • 你可以在後端加上 CORS 的設定,讓瀏覽器知道你允許哪些網域可以存取你的 API

接下來,我們就要來替前一章節的 API 加上 CORS 的設定。

CORS 設定

首先我們先回顧一下前一章節寫的範例程式碼

const express = require('express');
const mongoose = require('mongoose');
const app = express();


let connectStatus = false;

async function connectMongoDB () {
  try {
    await mongoose.connect('mongodb+srv://url')
    console.log('Connected to MongoDB...')
    connectStatus = true;
  } catch (error) {
    console.log(error)
  }
}

connectMongoDB()

app.use(express.json());

app.use((req, res, next) => {
  if (connectStatus) {
    next();
  } else {
    res.status(503).send({
      status: false,
      message: 'Server is not ready'
    });
  }
})

const todoSchema = new mongoose.Schema({
  id: Number,
  title: String,
  completed: Boolean,
},{
  versionKey: false,
  _id: false,
});

const Todo = mongoose.model('Todo', todoSchema);

app.get('/todos', async (req, res) => {
  const todos = await Todo.find().select('-__v -_id');
  res.send({
    status: true,
    data: todos,
  });
});

app.post('/todos', async (req, res) => {
  const { title, completed } = req.body;

  const todo = new Todo({
    id: new Date().getTime(),
    title,
    completed,
  });

  await todo.save();
  res.send({
    status: true,
    message: 'Create todo successfully',
  });
});

app.listen(3000)

那麼為什麼要加上 CORS 設定呢?主要原因是跟目前的主流開發模式有關,現在的主流開發模式是前後端分離,也就是說前端會獨立出來,後端也會獨立出來,這樣子的好處是可以讓前後端的開發團隊可以獨立開發,不會互相影響,彼此著重在自己的領域上就好。

因此為了解決這個問題,後端就必須要加上 CORS 的設定,讓前端可以存取後端的 API。

起手式很簡單,由於我們是使用 Express,所以我們可以使用 cors 這個套件來幫我們處理 CORS 的問題。

npm i cors

接著我們就可以在我們的程式碼中加上 CORS 的設定

const express = require('express');
// 加入 cors 套件
const cors = require('cors');

// ...略過其他程式碼

// 加入 cors 設定
app.use(cors());

// ...略過其他程式碼

app.listen(3000);

這樣子就完成了,很簡單對吧?那麼這時候你可能會想說...

「可是我在前面用 Postman 測試的時候,並沒有遇到 CORS 的問題啊?」

其實原因是因為 CORS 這個問題只會發生在瀏覽器上,而 Postman 本身是一套軟體,你也可以把它看成一個後端的程式,因此它不會有 CORS 的問題,這也是為什麼有時候我們在跟後端溝通時,用 Postman 溝通 API 時,都沒有問題,但是當我們把 API 串接到前端時,就會遇到 CORS 的問題。

但是上面的 CORS 設定並不是那麼的正確,因為上面的設定是允許所有網域存取你的 API

{
  "origin": "*",
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
  "preflightContinue": false,
  "optionsSuccessStatus": 204
}

Note
上面的意思是允許所有網域存取你的 API,並且允許的方法有 GET,HEAD,PUT,PATCH,POST,DELETE

就會有安全性的問題,因此我們必須要限制存取的網域。

那麼我們可以透過 origin 這個參數來限制存取的網域,例如我們只允許 http://localhost:8080 這個網域存取我們的 API,那麼我們可以這樣子寫

const express = require('express');
// 加入 cors 套件
const cors = require('cors');

// ...略過其他程式碼

const corsOptions = {
  origin: 'http://localhost:8080',
};

app.use(cors(corsOptions));

// ...略過其他程式碼

app.listen(3000);

Note
有些比較舊的瀏覽器必須額外加上 optionsSuccessStatus: 200 才能正確運作,但是現在的瀏覽器都已經不需要了,所以這邊就不多做介紹。

corsOptions.origin 這個參數可以接受一個字串或是一個函式,如果是字串的話,就是指定一個網域,如果是函式的話,就可以自己定義邏輯,例如我們可以這樣子寫

const express = require('express');

// 加入 cors 套件
const cors = require('cors');

// ...略過其他程式碼

const whitelist = ['http://localhost:8080', 'http://localhost:8081'];
const corsOptions = {
  origin(origin, callback) {
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}

app.use(cors(corsOptions))

// ...略過其他程式碼

app.listen(3000);

這樣子就可以自己定義邏輯了,這邊我們定義了一個 whitelist 陣列,裡面放了兩個網域,然後我們在 corsOptions 中定義了一個 origin 函式,這個函式會接收兩個參數,分別是 origincallbackorigin 代表的是存取的網域,而 callback 則是一個回呼函式,我們可以透過這個回呼函式來決定是否允許存取。

Note
whitelist 意指白名單,也就是說我們只允許白名單中的網域存取我們的 API。

當然,如果你不想用 cors 套件的話,你也可以試著自己寫 Middleware 來處理 CORS 的問題

const express = require('express');

const app = express();

// ...略過其他程式碼

const whitelist = ['http://localhost:8080', 'http://localhost:8081'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (whitelist.indexOf(origin) !== -1) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    next();
  } else {
    res.status(403).send({
      status: false,
      message: 'Not allowed by CORS'
    });
  }
});

// ...略過其他程式碼

app.listen(3000);

只是我們還是比較常見用 cors 套件來處理 CORS 的問題,因為這樣子比較簡單。

Environment Variable

Environment Variable 中文是「環境變數」,對於某些人來講,可能會無法知道環境變數的用途,但是對於有些人來講,環境變數是一個很重要的東西,因為環境變數可以讓我們的程式更加的彈性,也可以讓我們的程式更加的安全。

雖然我們前面介紹了不少東西,但是 Environment Variable 依然是一個很重要的東西,因此我們必須要學習如何使用 Environment Variable。

那麼環境變數主要是用來做什麼的呢?底下我也列出常見的用途

  • 機敏資訊的隱藏
  • 環境的切換
  • 增加可維護性

機敏資訊的隱藏

舉凡資料庫的密碼、金鑰、API 金鑰等等,這些資訊都是機敏資訊,我們不希望這些資訊被外人知道,因此我們可以透過環境變數來隱藏這些資訊。

如果你寫死在程式碼中的話,那麼一旦你的程式碼被外人取得,那麼你的資料庫就會被入侵,因此我們必須要把這些機敏資訊隱藏起來。

環境的切換

有時候我們某些程式碼是只能在特定環境下運作的,那麼我們就可以透過環境變數來切換環境,例如我們可以透過環境變數來切換資料庫,這樣子就可以確保我們測試的環境跟正式環境是不同的。

增加可維護性

「什麼?!使用環境變數也可以增加可環護性?」

你沒有看錯,假設有一段程式碼是這樣子的

// a.js
axios.get('http://localhost:3000/api/v1/profile');

// b.js
axios.get('http://localhost:3000/api/v1/users');

// c.js
axios.get('http://localhost:3000/api/v1/posts');

你可以看到上面的程式碼都是存取 http://localhost:3000 這個網域,但是如果有一天我們要把網域改成 http://localhost:3001 的話,那麼我們就必須要把所有的程式碼都改一遍,因此透過環境變數可以讓我們的程式碼更加的彈性,也可以讓我們的程式碼更加的可維護。

https://ithelp.ithome.com.tw/upload/images/20230915/20119486MFZqmkcXuC.png

Note
axios 是一個 HTTP 請求套件。

環境變數的設定

首先一開始我們要來安裝一個套件,也就是 dotenv 套件,這個套件可以讓我們在程式碼中使用環境變數。

npm i dotenv

接著我們要來建立一個 .env 檔案,這個檔案就是用來存放環境變數的地方,我們可以在這個檔案中定義環境變數,例如我們可以這樣子寫

PORT=3000

這樣子就定義了一個 PORT 的環境變數,接著我們要來修改我們的程式碼,讓我們的程式碼可以使用環境變數。

const express = require('express');

// 加入 dotenv 套件
require('dotenv').config();

const app = express();

// ...略過其他程式碼

app.listen(process.env.PORT);

這樣子就可以使用環境變數了,這邊我們使用了 process.env.PORT 這個環境變數,這個環境變數就是我們在 .env 檔案中定義的 PORT 環境變數。

Note
process.env 是 Node.js 中的一個全域變數,可以用來存放環境變數;環境變數的命名通常會以大寫字母為主,例如:PORTDB_URLAPI_KEY 等等。

那麼剛剛也有提到可以增加可維護性,那麼我們就來看看怎麼使用環境變數來增加可維護性

// a.js
axios.get('http://localhost:3000/api/v1/profile');

// b.js
axios.get('http://localhost:3000/api/v1/users');

// c.js
axios.get('http://localhost:3000/api/v1/posts');

我們可以看到上面的程式碼都是存取 http://localhost:3000 這個網域,但是如果有一天我們要把網域改成 http://localhost:3001 的話,那麼我們就必須要把所有的程式碼都改一遍,這時候我們就可以透過環境變數來解決這個問題,只需要在 .env 檔案中定義一個 API_URL 環境變數,然後在程式碼中使用這個環境變數就可以了。

API_URL=http://localhost:3000
// a.js
axios.get(`${process.env.API_URL}/api/v1/profile`);
// b.js
axios.get(`${process.env.API_URL}/api/v1/users`);
// c.js
axios.get(`${process.env.API_URL}/api/v1/posts`);

很輕鬆吧?這樣子就可以增加可維護性了。

那麼這邊也提一下,為什麼要使用 dotenv 套件來使用環境變數呢?因為 dotenv 套件可以讓我們在程式碼中使用環境變數,而且 dotenv 套件會自動幫我們讀取 .env 檔案,並且把 .env 檔案中的環境變數加到 process.env 中,這樣子我們就可以在程式碼中使用環境變數了。

因此這邊最後做個結論,Node.js 本身就內建 process.env 的環境變數物件,但 dotenv 套件提供了更好的方式來管理、組織和使用環境變數。

那麼這一篇就先到這邊結束,我們下一篇見哩。

碎碎念

其實 Env 官方後來有出一個 Dotenv Vault,我認為還滿不錯用的,詳情可以參考這裡唷~


上一篇
Day11 - Express 與 Mongoose
下一篇
Day13-替你的 Express 戴上頭盔吧!
系列文
《Node.js 不負責系列:把前端人員當作後端來用,就算是前端也能嘗試寫的後端~原來 Node.js 可以做這麼多事~》31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言