API作為資料通訊的大門,我們應該重視它的安全性,即便有些API可以很輕鬆地去進行調用,但不代表應該讓所有API都不受限制地被存取調用。

API可以做很多事,資料請求、處理敏感資訊、CRUD資料庫,甚至可以透過API控制電子設備,因此在APP開發上,針對API做嚴格的身份驗證及管理就會顯得格外重要。

Authentication & Authorization

當收到API請求時,通常會先進行 Authentication以及Authorization 身份驗證以及授權,成功授權才可以進行請求,

  • Authentication(驗證): 顧名思義就是驗證使用者的身份是否正確,可透過username, password等方式進行驗證,這些資訊可在使用者的請求資訊中取得。
  • Authorization(授權):當身份驗證成功,就能對進行授權,回傳資料給使用者。

1. 透過JWT實現授權

JWT (Json Web Token),通常在使用者成功登入後(驗證帳號密碼),會透過JWT產生一組具有時效性的Token給使用者(可儲存於cookies或localStorage),之後使用者在操作一些功能時必須將token一並附上才可使用。

生成JWT 程式範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const jwt = require('jsonwebtoken');

// 用戶資訊,可以是任何你想要儲存在 token 中的資訊
const user = {
id: 1, // 假設的用戶 ID
username: "exampleUser",
email: "user@example.com"
};

// 密鑰,用於簽名 JWT,應保持秘密
const secretKey = "yourSecretKey";

// 生成 JWT
const token = jwt.sign(user, secretKey, {
expiresIn: '1h' // token 有效期,這裡設為 1 小時
});

console.log(token);

當使用者欲發送API請求,需要將登入後拿到的token一併附上,server會透過此token進行驗證,並給予授權。

驗證JWT 程式範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 替換成你的密鑰
const secretKey = "yourSecretKey";

// JWT 驗證 Middleware
const verifyToken = (req, res, next) => {
// 從請求頭部取得 token
const token = req.headers['authorization'];

if (!token) {
return res.status(403).send({ message: "未提供 token!" });
}

try {
// 驗證 token
const decoded = jwt.verify(token, secretKey);
// 將用戶資訊附加到請求對象
req.user = decoded;
next();
} catch (error) {
// 如果驗證失敗,返回錯誤訊息
return res.status(401).send({ message: "無效的 token!" });
}
};

2. 細粒度存取控制Fine-Grained Access Control

定義角色和權限

定義每個角色可以做什麼事情,像是Admin可以進入控制台介面,User則不行。

RBAC (Role-Based Access Control)

RBAC是一種基於角色的訪問控制機制,每個角色都被授予一些權限,根據用戶的角色來決定對系統資源的訪問權限。在這種模式下,訪問權限會被賦予角色,用戶會被指派到一個或多個角色,這樣就能獲得相應的訪問權限。

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
// 假設的用戶資料和角色
const users = [
{ id: 1, username: 'Lando', role: 'admin' },
{ id: 2, username: 'Josh', role: 'user' }
];

// 簡單的身份驗證中間件
function authMiddleware(req, res, next) {
const user = users.find(u => u.username === req.headers.username); // 假設使用username進行簡單的"身份驗證"
if (!user) {
return res.status(401).send('Authentication failed');
}
req.user = user;
next();
}

// 已定義好的角色權限
const rolesPermissions = {
admin: ['create', 'read', 'update', 'delete'],
user: ['read']
};

// RBAC中間件
function roleMiddleware(allowedRoles) {
return function(req, res, next) {
const userRole = req.user.role;
const permissions = rolesPermissions[userRole] || [];
const isAllowed = allowedRoles.some(role => permissions.includes(role));
if (!isAllowed) {
return res.status(403).send('Access denied');
}
next();
};
}

app.get('/data', authMiddleware, roleMiddleware(['read']), (req, res) => {
res.send('Some data accessible by users with read permission');
});

ABAC (Attribute-Based Access Control)

ABAC能夠提供基於多種條件的細粒度訪問控制,當需要根據用戶的具體屬性(年齡、位置、工作角色等)來細化訪問權限,通常會使用於更複雜或是需要極度安全性的場景,例如軍事系統、政府等。

3. API Gateway提升安全性

你可以把API網關想像成所有API的前門,所有請求都必須先通過前門才碰得到後面的API,舉個例子,假如每個API都有一樣的身份驗證middleware,這樣只需要在API gateway加入身份驗證,如此一來所有請求都會先進行身份驗證後,再將請求轉發到相應的API route。

API網關有以下用途:

  • 增加安全性:防止未經授權的請求、濫用,或是DDoS攻擊。

  • Rate Limiting / Throttling:速率控制和節流,防止API過度使用,或是確保使用者之間的公平使用,可使用express-rate-limit套件或是其他平台的服務,例如AWS API Gateway, Google Apigee等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const rateLimit = require('express-rate-limit');

    // 創建 rate limit 中間件
    const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 分鐘的時間窗口
    max: 100, // 在時間窗口內每個IP可以發起的最大請求數
    standardHeaders: true, // 返回 rate limit 信息於 `RateLimit-*` headers
    legacyHeaders: false, // 禁用 `X-RateLimit-*` headers
    message: '請求過於頻繁,請在一段時間後再試。' // 當 rate limit 被觸發時返回的錯誤信息
    });

    // 使用這個中間件於你的應用中,例如
    // app.use(limiter);

    // 你也可以只對特定的路由使用這個中間件
    // app.use('/api/', limiter);
  • 資料驗證:驗證傳入的資料是否符合格式標準,亦可預防SQL Injection等攻擊。

4. DOMPurify

當我們設計一個Input的元件給客戶端做輸入時,也有機率遭受到攻擊,因為input可以被寫入程式碼,例如:<script>alert(false)</script>,這時我們可以使用DOMPurify套件來清除輸入的資料,避免XSS攻擊。

安裝

1
2
3
4
5
// 先執行命令進行安裝
$ npm install dompurify

// JS 檔案中 import
import * as DOMPurify from 'dompurify';

使用

假如我們有一個input,取得使用者資訊

1
<input type="text" id="userName">

將資訊傳送給後端時先透過DOMPurify處理過

1
2
const input = document.getElementById('userName');
const userName = DOMPurify.sanitize(input.value);

這樣就可以清除程式碼的注入,多提升了一點安全性。

5. 透過SSL/TLS加密傳輸資料

ZeroSSL網站為你的網域申請SSL憑證,申請成功會拿到certificate.crt以及private.key兩個憑證檔案,自行將其放入伺服器資料夾(我是放到 /etc/ssl 目錄底下 ),再透過nginx.conf設定SSL憑證路徑,保存後執行Nginx即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 443 ssl;
server_name yourdomain.com;

ssl_certificate /path/to/your/certificate.crt; # Path to your fullchain.pem from CA
ssl_certificate_key /path/to/your/private.key; # Path to your private key from CA

# 指定TLS版本
ssl_protocols TLSv1.2 TLSv1.3;
# 定義加密套件
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256...';

# 其餘設定
}
  • ssl_protocols : 指定TLS版本亦可提升安全性,在此指定1.2跟1.3版本的連線,因為這些是目前被認為是安全的協議版本,較舊的協議版本(如 SSL v2、SSL v3、TLS v1.0)因已知的安全漏洞而不建議使用。
  • ssl_ciphers : 這個指令用來定義哪些加密套件(cipher suites)被允許用於 SSL/TLS 連線。加密套件定義了數據加密、驗證和消息完整性檢查的算法。

除了像SSL這種加密傳輸的方式以外,也有一些靜態加密的方法

  • bcrypt : 加密套件,在存入資料庫之前先對資料進加密。
  • MySQL : InnoDB表空間加密,在資料寫入磁碟前被加密,從磁碟讀取時解密。
  • MongoDB : 加密儲存引擎,也是在儲存資料之前先將其加密。

總結

提升API的安全性除了能夠保障你的數位資產,同時也能夠保障用戶對你的信任,在開發API前不妨連同安全性也一起考慮進去,以防範各種網路威脅。