昨天完整的介紹了 router 路由,它幫助我們輕易的組裝出 API。完成此目的三個核心概念是 中間件(middleware)、路由(routing)、流(stream),其中 middleware 是更加的重要。
因為 middleware 可以做很多事,例如:
router.use(middleware
、 router.METHOD(path, middleware)
、 app.use(middleware
和 app.METHOD(path, middleware)
中。app.use(express.json())
放在越前面,可以使後面串進的 router 都可以被 express.json()
這 middleware 作用(req.body
解析成 JSON object)。express.json()
就是跟 JSON 有關,越直覺的命名可以增加程式碼可讀性。下面雖然串更多 middleware,但意思很明顯
router.post('/api/accounts', LogMiddleware, AuthMiddleware, AddAccountMiddleware, WrappedDataEndpointMiddleware)
我們採用的模式是,認証完成後回應 token 給 client,client 自己留著,每當client要對後端操作時會帶著 token 一併給後端,而不是用在後端保留登入資訊(session)。
這樣做有個好處是後端是無狀態的(stateless),任何一個台可以識別 token 的後端都可以服務 client,以提高後端的可擴展性(scalability)。
基本的認証機制有幾個步驟(與上圖對應)
雖然看起來很單純但在這些環節中有些資安注意事項
httpOnly
(Cookie只能被伺服端存取,client 無法用 javascript 讀取)、secure
(只能透過https的方式傳輸)token 是經過認証單位認証後,所簽發(sign)的字串。 client 拿到 token 後當要求後端服務時一併送出,後端就可以依 token 識別身份給與服務。
我們要採用 JWT (JSON Web Token) 做為 token 的資料格式。
它是由三個部分組成的
標頭(Header).內容(Payload).簽名(Signature)
標頭(Header):Base64編碼的字串。一般內含兩個屬性:token 類型、雜湊(hashing)函數的名字(ex: HMAC SHA256 or RSA),如:
{
"alg": "HS256",
"typ": "JWT"
}
再透過 Base64Url 編碼,一般在轉換前,會把不可見字元(ex: 空白, 換行)拿掉
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
就是 Header。
內容(Payload):Base64編碼的字串。內含一堆 Claims,像是:(截錄自JSON Web Token (JWT) 簡介)
iss
: The issuer of the token,token 是給誰的sub
: The subject of the token,token 主題exp
: Expiration Time。 token 過期時間,Unix 時間戳記iat
: Issued At。 token 建立時間, Unix 時間戳記jti
: JWT ID。針對當前 token 的唯一標識上面只列出一些 JTW 定義的 claims,其它見 IANA JSON Web Token Registry。你也可以自己放任何的資料。來個 Payload 可能資訊
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
再透過 Base64Url 編碼,一般在轉換前,會把不可見字元(ex: 空白, 換行)拿掉
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
就是 Payload。
到目前為止的 Header 和 Payload 雖然是 Base64Url 編碼後,但它們 可以解碼,所以也算是明文資料,不應該放敏感資料。
簽名(Signature):拿 Header、Payload和一個密鑰(secret)當參數,經過不可反解的雜湊函數後得到。以 HMAC SHA256 來說
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
這裡 header, payload是指未經過 Base64Url 編碼, secret 是 your-256-bit-secret
,產生:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
最後的 JTW 就是
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT首頁 有 JTW 加解密互動網頁可以玩玩看。
要注意,這個 secret 要好好保存在後端,這是拿來判斷 JWT 是否有效的鑰匙。
HMACSHA256(
JWT.header + "." +
JWT.payload,
secret)
看結果和 Signature 一不一樣,就可以判斷 JWT 是否有效。最後要注意一件是:
JWT 解決的是簽証(sign)安全,不是傳輸全安全,要配合加密通道(ex: https)才能安全地傳遞 JWT。
JWT首頁 有不同程式語言的實作套件,直接用就可以了。
Node.js 用的是 jsonwebtoken。
過程請見 github commit log ithelp-30dayfullstack-Day19
執行前需要先開 mongodb (npm run startdb
),不用時可以關 mongodb (npm run stopdb
)
我們例用 JWT 來實作認証機制
POST /api/auth/login
這 api 是用來登入POST /api/echo
這 api 需要有 token 才能運作,利用middleware 輕鬆掛入POST /api/auth/login
加入 ./routers/AuthRouter.js
專們處理受權相關
const express = require('express');
/**
*
* @param {object} dependencies
*/
function createRouter(dependencies) {
// Get dependencies
const { } = dependencies;
// Create a router
var router = express.Router();
/* POST log */
router.post('/login', function (req, res, next) {
next(new Error('Not implement'));
});
return router;
}
module.exports = {
createRouter
};
串入 root router
串入root router (不懂的請見:Day 18 - 二周目 - 剖析 express 路由(router) 三概念:中間件(middleware)、路由(routing)、流(stream))
// routers/index.js
router.use('/api/auth', authRouter);
authRouter 物件設定 (不懂的請見:Day 17 - 二周目 - 依賴注入與組態化專案)
// app.js
const { createRouter: createAuthRouter } = require('./routes/AuthRouter');
container.register({
...略
authRouter: asFunction(createAuthRouter, { lifetime: Lifetime.SINGLETON }),
});
Postman 打看看
帳密比對實作
我們假設 verifyUser()
會做資料庫查詢
// routers/AuthRouter.js
async function verifyUser(data) {
const username = _.get(data, 'username');
const password = _.get(data, 'password');
if(username === 'billy' && password === '1234') { // pass
return Promise.resolve({
username,
email: 'billy@gmail.com',
});
}
return Promise.reject(new Error('Fail'));
}
套用帳密比對
// routers/AuthRouter.js
router.post('/login', function (req, res, next) {
const data = req.body;
verifyUser(data)
.then(user => {
res.json(user);
})
.catch(next);
});
到目前我們做出簡單的帳密驗証。接下來,我們要為驗証成功的 client 回傳 JWT
我們約定把 JWT 儲存在client 的 cookie中
npm install jsonwebtoken --save
// routers/AuthRouter.js
const EXPIRES_IN = 10 * 1000; // 10 sec
const SECRET = 'YOUR_JWT_SECRET';
router.post('/login', function (req, res, next) {
console.log(JSON.stringify(req.cookies)); // 印出 cookies
const data = req.body;
verifyUser(data)
.then(user => {
const token = jwt.sign(user, SECRET, { expiresIn: EXPIRES_IN });
res.json({
token
});
})
.catch(next);
});
這裡用 expiresIn
選項可以方便地指定簽發的 JWT 多久到期,像我們設 10 秒。res.cookie('token', token, { maxAge: EXPIRES_IN, httpOnly: true}); // 回應 client ,把 token 存在名為 token 的 cookie 並設定相關屬性
再試打一下,查看 client(Postman) 收到回應的 headersHttpOnly
設定時,cookie token
不能用 javascript 取出。但你可以在 Postman 的 Cookies,可以查看所有 cookies,你會看到 token
被設定
另外,觀察到:
EXPIRES_IN
),所以時間到後再看一次就會消失。{}
,第二次打,因為前一次登入成功並設定cookies,所以就會有 cookies。POST /api/echo
加入 token 驗証假設 client 會把 JWT 放在 名為 token
的 cookie 中,所以後端可以由
req.cookies.token
得到來自 client 的 token。因此,我們只要驗証此 token 就可以知道,request 是否有授權。
我們利用 middleware 來做 JWT 的驗証
VerifyJWT
,當 VerifyJWT()
動態產生 middelware
// middlewares/VerifyJWT.js
const _ = require('lodash');
const jsonwebtoken = require('jsonwebtoken');
const SECRET = 'YOUR_JWT_SECRET'; // 要和簽發時一樣,所以可以放在 ./configs/config.js 中
async function verifyJWT(jwt) {
if (!jwt) {
return Promise.reject(new Error('No JWT'));
}
const decoded = jsonwebtoken.verify(jwt, SECRET);
return decoded;
}
module.exports = function (options = {}) {
const {tokenPath = 'cookies.token'} = options; // tokenPath 是取出 token 的路徑
return function (req, res, next) {
const jwt = _.get(req, tokenPath);
verifyJWT(jwt)
.then(decoded => {
console.log(decoded);
next(); // next middleware
})
.catch(next);
};
}
我們很刻意的利用閉包技巧,輸入 tokenPath
來動態產生 middelware。使用時,VerifyJWT()
才是 middelware 的簽章。POST /api/echo
掛入 JWT 驗証
router.post('/api/echo', VerifyJWT(), function (req, res, next) {
...略
}
這樣就完成對POST /api/echo
掛入 JWT 驗証,要有 JWT 才能執行這支API。
今天介紹基本認証的機制,還利用 JWT 來傳遞驗証結果。最後,利用 middleware 可以方便的掛入需要驗証的 APIs.
未來有機會在來談 passport.js、OAuth、Time-based One-Time Password(TOTP)二階段驗証。