Day12 - 關於 CORS 與 Env

關於 CORS 與 Env

前言

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

Cross-origin resource sharing?

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

1
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 設定

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 的問題。

1
npm i cors

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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
{
"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,那麼我們可以這樣子寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 這個參數可以接受一個字串或是一個函式,如果是字串的話,就是指定一個網域,如果是函式的話,就可以自己定義邏輯,例如我們可以這樣子寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 的問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 金鑰等等,這些資訊都是機敏資訊,我們不希望這些資訊被外人知道,因此我們可以透過環境變數來隱藏這些資訊。

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

環境的切換

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

增加可維護性

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

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

1
2
3
4
5
6
7
8
// 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 的話,那麼我們就必須要把所有的程式碼都改一遍,因此透過環境變數可以讓我們的程式碼更加的彈性,也可以讓我們的程式碼更加的可維護。

又少改一個地方了

Note
axios 是一個 HTTP 請求套件。

環境變數的設定

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

1
npm i dotenv

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

1
PORT=3000

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

1
2
3
4
5
6
7
8
9
10
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 等等。

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

1
2
3
4
5
6
7
8
// 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 環境變數,然後在程式碼中使用這個環境變數就可以了。

1
API_URL=http://localhost:3000
1
2
3
4
5
6
// 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,我認為還滿不錯用的,詳情可以參考這裡唷~