iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 21 篇

Day 21|讓 API 只有登入的人能使用:JWT Middleware

  • 分享至 

  • xImage
  •  

前言

前面已經完成 JWT 登入流程,使用者登入成功後,後端會簽發一組 Token。

但拿到 Token 之後,事情還沒結束。

之後使用者每次呼叫 API,後端都需要先確認:「這個人有沒有登入?帶來的 Token 有沒有問題?」

例如:

GET /api/notes

假設這支 API 只開放給登入使用者使用,Request 進來時,後端就要先檢查 JWT。如果 Token 不存在或驗證失敗,就直接擋下來;確認沒問題後,才讓 Request 繼續執行。

如果每一支 API 都自己寫一次 JWT 驗證,很快就會出現一堆重複程式碼。這時候就可以把驗證的工作交給 Middleware,讓所有需要登入的 API 共用同一套檢查流程。

Authorization Header 和 Bearer Token

前端拿到 JWT 之後,呼叫需要登入的 API 時,通常會把 Token 放進 Authorization Header。

格式會長這樣:

Authorization: Bearer <JWT>

Authorization 是 Header 的名稱,Bearer 表示後面帶的是一組 Token,最後的 <JWT> 才是真正的 JWT。

例如:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

前端可能會這樣送出:

fetch('/api/notes', {
  headers: {
    Authorization: `Bearer ${token}`
  }
});

所以當這個 Request 到達後端時,Express 就可以從 req.headers.authorization 取得整串內容。

接下來的工作,就是把這串內容拆開,拿到真正的 Token,再交給 jwt.verify() 檢查。

verifyToken:先確認你有沒有登入

這次要做的 Middleware 可以放在:

middlewares/
└── auth.js

完整程式碼如下:

const jwt = require('jsonwebtoken');

function verifyToken(req, res, next) {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
      return res.status(401).json({
        message: 'Unauthorized'
      });
    }

    const [type, token] = authHeader.split(' ');

    if (type !== 'Bearer' || !token) {
      return res.status(401).json({
        message: 'Unauthorized'
      });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    req.user = decoded;

    next();
  } catch (error) {
    return res.status(401).json({
      message: 'Unauthorized'
    });
  }
}

module.exports = verifyToken;

這段 Middleware 可以按照 Request 進來之後的順序來看。

首先:

const authHeader = req.headers.authorization;

從 Request Header 裡取得 Authorization。

假設收到的是:

Bearer eyJhbGciOiJIUzI1NiIs...

接著用:

const [type, token] = authHeader.split(' ');

按照中間的空白切開。

原本:

Bearer eyJhbGciOiJIUzI1NiIs...

會變成:

type  → Bearer
token → eyJhbGciOiJIUzI1NiIs...

所以這裡才會檢查:

if (type !== 'Bearer' || !token) {
  return res.status(401).json({
    message: 'Unauthorized'
  });
}

如果沒有 Authorization,或格式不是 Bearer <Token>,就直接回傳 401 Unauthorized,Request 也不會繼續往下走。

格式確認沒問題之後,才是真正的 JWT 驗證:

const decoded = jwt.verify(token, process.env.JWT_SECRET);

jwt.verify() 會檢查 Token 是否有效,例如簽章是否正確、內容有沒有被竄改,以及 Token 有沒有過期。如果驗證失敗,就會進到 catch,最後回傳 401 Unauthorized。

只有驗證成功,才會繼續往下。

為什麼要放進 req.user?

JWT 驗證成功後,jwt.verify() 會把 JWT 裡的 Payload 回傳出來,這個結果目前放在 decoded 裡。

例如前面登入時簽發 JWT:

const token = jwt.sign(
  {
    userId: user.id
  },
  process.env.JWT_SECRET,
  {
    expiresIn: '1h'
  }
);

驗證之後,可能會得到:

{
  userId: 123,
  iat: 1759570000,
  exp: 1759573600
}

這個 decoded 就是已經通過驗證的 JWT Payload。

接著:

req.user = decoded;

這一步的目的,是把驗證成功的使用者資訊交給後面的程式。

因為 decoded 只是 verifyToken 裡面的變數,後面的 Controller 並不能直接使用它。Middleware 執行完之後會呼叫 next(),讓 Request 繼續往下走,而後面的程式拿到的還是同一個 req,所以可以把資料放進 req.user,讓後面的 Controller 直接取用。

流程可以想成:

jwt.verify()
    ↓
得到 decoded
    ↓
req.user = decoded
    ↓
next()
    ↓
Controller

因此 Controller 裡就能直接寫:

const userId = req.user.userId;

拿到目前登入使用者的 userId。

這裡的 req.user 可以把它想成 Middleware 留給後面程式的「使用者身分資訊」。Middleware 先確認 Token 沒問題,再把確認過的結果交給後面的程式,Controller 就不用重新處理 JWT。

最後的:

next();

代表這一關通過了,Request 可以繼續往下一個 Middleware 或 Route Handler 前進。

Protected Route:通過驗證才能進來

有了 verifyToken 之後,就可以把它放到需要登入的 Route。

例如筆記 API:

const express = require('express');
const verifyToken = require('../middlewares/auth');

const router = express.Router();

router.get('/', verifyToken, async (req, res, next) => {
  try {
    const userId = req.user.userId;

    const notes = await getNotesByUserId(userId);

    res.json({
      data: notes
    });
  } catch (error) {
    next(error);
  }
});

module.exports = router;

這裡最重要的是:

router.get('/', verifyToken, async (req, res, next) => {

當使用者呼叫:

GET /api/notes

Request 不會直接進入後面的 Route Handler,而是先經過 verifyToken。

如果沒有 Token,或 Token 驗證失敗,Middleware 就會回傳 401,流程停在這裡。

如果驗證成功,Middleware 會把使用者資訊放進 req.user,再呼叫 next(),Route Handler 才會繼續執行。

所以到了這裡:

const userId = req.user.userId;

就能知道目前是哪個使用者,接著再拿 userId 去查詢他的筆記。

這也是 Middleware 很適合處理驗證的原因:需要登入的 API 都可以共用 verifyToken,Controller 只需要專心處理自己的工作。

整個流程其實就是:

Request
  ↓
verifyToken
  ↓
檢查 Authorization
  ↓
取得 Token
  ↓
jwt.verify()
  ↓
驗證失敗 → 401
  ↓
驗證成功
  ↓
req.user = decoded
  ↓
next()
  ↓
Controller

到了這裡,JWT、Middleware 和 API 就真正接起來了。

把整個流程串起來

回頭看一次完整流程。

使用者先登入:

POST /auth/login
      ↓
驗證帳號密碼
      ↓
簽發 JWT
      ↓
前端取得 Token

之後呼叫需要登入的 API:

GET /api/notes
      ↓
Authorization: Bearer <JWT>
      ↓
verifyToken
      ↓
取得 Token
      ↓
jwt.verify()
      ↓
驗證成功
      ↓
req.user = decoded
      ↓
next()
      ↓
Controller
      ↓
req.user.userId
      ↓
查詢使用者的資料
      ↓
回傳 Response

如果中間任何一步驗證失敗,就會直接回傳 401,Request 不會進入 Controller。

結論

JWT Middleware 做的事情其實很單純:先確認 Request 有沒有帶 Token,再確認這組 Token 是否有效。驗證成功後,把使用者資訊放進 req.user,讓後面的 Controller 可以直接使用。

這樣一來,需要登入的 API 就不用各自處理 JWT,統一交給 Middleware 驗證即可。Middleware 負責把關 API 的入口,Controller 則接手後面的資料處理。

把這整段記起來就好:

JWT 帶著身分資訊進來,Middleware 負責驗證,req.user 把驗證結果交給後面的 API。

到這裡,使用者的身分已經能被驗證,API 也有了一道基本的入口檢查。接下來把注意力放到資料庫,看看當一次操作需要處理好幾筆資料時,怎麼確保資料不會只完成一半。


上一篇
Day 20|從登入到身分驗證:認識 JWT
下一篇
Day 22|一次操作很多筆資料,怎麼確保不出錯?認識 Transaction
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言