iT邦幫忙

2022 iThome 鐵人賽

DAY 25
2
Software Development

Moleculer 家家酒系列 第 25

Day 25 : API 閘道器 - Part 2

  • 分享至 

  • xImage
  •  

API 閘道器 - Part 2

昨天介紹了閘道器的路由相關功能,今天要來談談它內建的各種屬性設定。

呼叫選項

路由中有一個 callOptions 屬性可以用來設定 broker.call 的屬性。你可以對其設定 timeoutretriesfallbackResponse ,它與 Actions 中的設定方式相同。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{

            callOptions: {
                timeout: 500,
                retries: 3,
                fallbackResponse(ctx, err) {
                    // ...
                }
            }
        }]
    }
});

多路由

你可以建立多個路由,它們可以分別設定不同的名稱、白名單、別名、呼叫選項及授權方式等。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [
            {
                path: "/admin",

                authorization: true,

                whitelist: [
                    "$node.*",
                    "users.*",
                ],

                bodyParsers: {
                    json: true
                }
            },
            {
                path: "/",

                whitelist: [
                    "posts.*",
                    "math.*",
                ],

                bodyParsers: {
                    json: true
                }
            }
        ]
    }
});

響應類型與狀態碼

當 Action 處理完成需要響應時,API 閘道器會偵測響應的類型,並且在 res 標頭設定 Content-Type 。預設的狀態碼是 200 。你也可以在 ctx.meta 覆蓋它來響應客製化的標頭及狀態碼。

可用的 meta 資訊:

  • ctx.meta.$statusCode - 設定狀態碼 res.statusCode
  • ctx.meta.$statusMessage - 設定狀態訊息 res.statusMessage
  • ctx.meta.$responseType - 設定標頭 Content-Type
  • ctx.meta.$responseHeaders - 設定標頭全部的 keys
  • ctx.meta.$location - 設定標頭重新導向 Location

範例:

module.exports = {
    name: "export",
    actions: {
        // 在瀏覽器響應檔案下載
        downloadCSV(ctx) {
            ctx.meta.$responseType = "text/csv";
            ctx.meta.$responseHeaders = {
                "Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`
            };
            
            return csvFileStream;
        },

        // 請求跳轉頁面
        redirectSample(ctx) {
            ctx.meta.$statusCode = 302;
            ctx.meta.$location = "/login";

            return;
        }
    }
}

授權

你可以很簡單的啟用授權功能,首先在路由設定中加入 authorization: true ,然後在服務中建立一個 authorize 函數。

const E = require("moleculer-web").Errors;

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            // 啟用授權
            authorization: true
        }]
    },

    methods: {
        // 建立授權函數
        authorize(ctx, route, req, res) {
            // 由標頭讀取 JWT Token
            let auth = req.headers["authorization"];
            if (auth && auth.startsWith("Bearer")) {
                let token = auth.slice(7);

                // 檢查 Token
                if (token == "123456") {
                    // 將使用者授權加到 `ctx.meta`
                    ctx.meta.user = { id: 1, name: "John Doe" };
                    return Promise.resolve(ctx);

                } else {
                    // 無效的 Token
                    return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
                }

            } else {
                // Token 不存在
                return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
            }
        }

    }
}

你可以參考官方原始碼中更詳細的 JWT 範例[2] 。

驗證

驗證的啟用方法類似於授權,首先在路由設定中加入 authentication: true ,然後在服務中建立一個 authenticate 函數。

返回的值將會被設定到 ctx.meta.user 屬性中,你可以在 Action 中利用它來取得已登入的使用者物件。

範例:

broker.createService({
    mixins: ApiGatewayService,

    settings: {
        routes: [{
            // 啟用驗證
            authentication: true
        }]
    },

    methods: {
        authenticate(ctx, route, req, res) {
            let accessToken = req.query["access_token"];
            if (accessToken) {
                if (accessToken === "12345") {
                    // 驗證憑證有效,將設定至 `ctx.meta.user`
                    return Promise.resolve({ id: 1, username: "john.doe", name: "John Doe" });
                } else {
                    // 無效的憑證
                    return Promise.reject();
                }
            } else {
                // 匿名訪客
                return Promise.resolve(null);
            }
        }
    }
});

路由 Hooks

路由也有前後呼叫的 Hooks 。你可以使用它來做一些像是設定 ctx.meta 、 訪問 req.headers 或是變更響應的 data

範例:

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [
            {
                path: "/",

                onBeforeCall(ctx, route, req, res) {
                    // 將請求的標頭設到 ctx.meta
                    ctx.meta.userAgent = req.headers["user-agent"];
                },

                onAfterCall(ctx, route, req, res, data) {
                    // 返回一個異步函數
                    return doSomething(ctx, res, data);
                }
            }
        ]
    }
});

在新的版本後,你可以在 onAfterCall 操作 data ,但是必須永遠返回一個新的或原始的 data

錯誤處理

你可以加入 route-levelglobal-level 兩個不同層級的客製化錯誤處理,並且在處理完畢後呼叫 res.end ,否則請求將不會被處理。

範例:

broker.createService({
    mixins: [ApiService],
    settings: {

        routes: [{
            path: "/api",

            // 路由錯誤處理
            onError(req, res, err) {
                res.setHeader("Content-Type", "application/json; charset=utf-8");
                res.writeHead(500);
                res.end(JSON.stringify(err));
            }
        }],

        // 全域錯誤處理
        onError(req, res, err) {
            res.setHeader("Content-Type", "text/plain");
            res.writeHead(501);
            res.end("Global error: " + err.message);
        }		
    }
}

錯誤格式化器

API 閘道器提供了一個格式化輔助函數,可以使用它來過濾不需要輸出的資料。

範例:

broker.createService({
    mixins: [ApiService],
    methods: {
        reformatError(err) {
            // 在發送到客戶端之前,先將錯誤資料做過濾
            return _.pick(err, ["name", "message", "code", "type", "data"]);
        },
    }
}

CORS 標頭

你可以在 API 閘道器中使用 CORS 標頭。

範例:

const svc = broker.createService({
    mixins: [ApiService],

    settings: {

        // 對所有路由使用全域的 CORS 設定
        cors: {
            // 設置 Access-Control-Allow-Origin CORS 標頭
            origin: "*",
            // 設置 Access-Control-Allow-Methods CORS 標頭
            methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
            // 設置 Access-Control-Allow-Headers CORS 標頭
            allowedHeaders: [],
            // 設置 Access-Control-Expose-Headers CORS 標頭
            exposedHeaders: [],
            // 設置 Access-Control-Allow-Credentials CORS 標頭
            credentials: false,
            // 設置 Access-Control-Max-Age CORS 標頭
            maxAge: 3600
        },

        routes: [{
            path: "/api",

            // 路由層的 CORS 設定 (會覆蓋全域設定)
            cors: {
                origin: ["http://localhost:3000", "https://localhost:4000"],
                methods: ["GET", "OPTIONS", "POST"],
                credentials: true
            },
        }]
    }
});

限速器

Moleculer-Web 內建了一個採用記憶體儲存方式的限速器。

範例:

const svc = broker.createService({
    mixins: [ApiService],

    settings: {
        rateLimit: {
            // 時間視窗,也是記憶體儲存的有效期限(毫秒)
            // 預設 60000 (1 分鐘)
            window: 60 * 1000,

            // 時間視窗中最大的請求數
            // 預設為 30 秒
            limit: 30,

            // 設定響應限速標頭
            // 預設為 false
            headers: true,

            // 密鑰生成函數,預設如下
            key: (req) => {
                return req.headers["x-forwarded-for"] ||
                    req.connection.remoteAddress ||
                    req.socket.remoteAddress ||
                    req.connection.socket.remoteAddress;
            },
            // StoreFactory: CustomStore
        }
    }
});

客製化儲存

class CustomStore {
    constructor(clearPeriod, opts) {
        this.hits = new Map();
        this.resetTime = Date.now() + clearPeriod;

        setInterval(() => {
            this.resetTime = Date.now() + clearPeriod;
            this.reset();
        }, clearPeriod);
    }

    /**
     * 由 key 增加計數
     *
     * @param {String} key
     * @returns {Number}
     */
    inc(key) {
        let counter = this.hits.get(key) || 0;
        counter++;
        this.hits.set(key, counter);
        return counter;
    }

    /**
     * 重設所有計數器
     */
    reset() {
        this.hits.clear();
    }
}

ETag

etag 是 HTTP 協定的一種快取驗證機制[3]。它可以設為 falsetrueweakstrong 或是一個客製化 Function 。更多請參閱 Code[4] 。

範例:

const ApiGateway = require("moleculer-web");

module.exports = {
    mixins: [ApiGateway],
    settings: {
        // 服務層級選項
        etag: false,
        routes: [
            {
                path: "/",
                // 路由層級選項
                etag: true
            }
        ]
    }
}

範例:客製化 etag Function

module.exports = {
    mixins: [ApiGateway],
    settings: {
        // 服務層級選項
        etag: (body) => generateHash(body)
    }
}

注意,此方法無法用於串流響應,你必須改為自行產生 etag

範例:客製化串流用 etag

module.exports = {
    name: "export",
    actions: {
        // 在瀏覽器響應檔案下載
        downloadCSV(ctx) {
            ctx.meta.$responseType = "text/csv";
            ctx.meta.$responseHeaders = {
                "Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`,
                "ETag": '<your etag here>'
            };
            return csvFileStream;
        }
    }
}

HTTP2 伺服器

API 閘道器提供了一個實驗性的 HTTP2 功能。你可以在服務設定中透過 http2: true 來啟用它。

範例:

const ApiGateway = require("moleculer-web");

module.exports = {
    mixins: [ApiGateway],
    settings: {
        port: 8443,

        // HTTPS 伺服器憑證
        https: {
            key: fs.readFileSync("key.pem"),
            cert: fs.readFileSync("cert.pem")
        },

        // 使用 HTTP2 伺服器
        http2: true
    }
});

使用 Express.js middleware

你可以將 API 閘道器作為一個 middleware 放到 Express.js 應用中。

const svc = broker.createService({
    mixins: [ApiService],

    settings: {
        server: false // 預設為 "true"
    }
});

// 建立 Express 應用程式
const app = express();

// 使用 ApiGateway 作為 middleware
app.use("/api", svc.express());

// 監聽
app.listen(3000);

// 啟動伺服器
broker.start();

完整的服務設定

列出 API 閘道器的所有設定

settings: {

    // 外部連接埠
    port: 3000,

    // 外部 IP
    ip: "0.0.0.0",

    // HTTPS 伺服器憑證
    https: {
        key: fs.readFileSync("ssl/key.pem"),
        cert: fs.readFileSync("ssl/cert.pem")
    },

    // 使用伺服器實例。如果設為 null 它將會建立一個新的 HTTP(s)(2) 伺服器,
    // 如果設為 false 則為作為 middleware 不啟動伺服器。
    server: true,

    // 外部全域基底路徑
    path: "/api",

    // 全域層級 middlewares
    use: [
        compression(),
        cookieParser()
    ],

    // 使用 'info' 層級來記錄請求的參數
    logRequestParams: "info",

    // 使用 'debug' 層級來記錄響應的資料
    logResponseData: "debug",

    // 使用 HTTP2 伺服器 (實驗性功能)
    http2: false,

    // 覆蓋 HTTP 伺服器的預設逾時
    httpServerTimeout: null,

    // 最佳化 route 與 別名路徑 (層數最深的優先)
    optimizeOrder: true,

    // 路由設定
    routes: [
        {
            // 路由的基底路徑 (完整路徑: /api/admin )
            path: "/admin",

            // Actions 的白名單 (字串遮罩陣列或正規表達式)
            whitelist: [
                "users.get",
                "$node.*"
            ],

            // 在呼叫 Action 前會先呼叫 `this.authorize` 方法
            authorization: true,

            // 合併 `querystring` 、 `params` 及 `body` 參數
            mergeParams: true,

            // 路由層級 middlewares
            use: [
                helmet(),
                passport.initialize()
            ],

            // Action 別名
            aliases: {
                "POST users": "users.create",
                "health": "$node.health"
            },

            // 映射策略
            mappingPolicy: "all",

            // 使用 bodyparser 模組
            bodyParsers: {
                json: true,
                urlencoded: { extended: true }
            }
        },
        {
            // 路由的基底路徑 (完整路徑: /api )
            path: "",

            // Actions 的白名單 (字串遮罩陣列或正規表達式)
            whitelist: [
                "posts.*",
                "file.*",
                /^math\.\w+$/
            ],

            // 無授權
            authorization: false,

            // Action 別名
            aliases: {
                "add": "math.add",
                "GET sub": "math.sub",
                "POST divide": "math.div",
                "GET greeter/:name": "test.greeter",
                "GET /": "test.hello",
                "POST upload"(req, res) {
                    this.parseUploadedFile(req, res);
                }
            },

            mappingPolicy: "restrict",

            // 使用 bodyparser 模組
            bodyParsers: {
                json: false,
                urlencoded: { extended: true }
            },

            // 呼叫選項
            callOptions: {
                timeout: 3000,
                retries: 3,
                fallbackResponse: "Static fallback response"
            },

            // 呼叫前的 Hook `broker.call`
            onBeforeCall(ctx, route, req, res) {
                ctx.meta.userAgent = req.headers["user-agent"];
            },

            // 呼叫後的 Hook `broker.call`
            onAfterCall(ctx, route, req, res, data) {
                res.setHeader("X-Custom-Header", "123456");
                return data;
            },

            // 路由錯誤處理
            onError(req, res, err) {
                res.setHeader("Content-Type", "text/plain");
                res.writeHead(err.code || 500);
                res.end("Route error: " + err.message);
            }
        }
    ],

    // 靜態檔案設定
    assets: {
        // assets 靜態檔案的根目錄路徑
        folder: "./examples/www/assets",

        // `serve-static` 模組的選項
        options: {}
    },

    // 全域錯誤處理
    onError(req, res, err) {
        res.setHeader("Content-Type", "text/plain");
        res.writeHead(err.code || 500);
        res.end("Global error: " + err.message);
    }
}

服務方法

addRoute 方法可以用來新增或取代一個路由。例如你可以由混和函數來呼叫它並且定義一個新的路由 (例如: swagger routegraphql route 等) 。

removeRoute 方法可以根據路徑來移除路由。(例如: this.removeRoute("/admin"); )

範例

Moleculer Web 官方還建立了許多的情境的範例,提供給開發者作為參考:

https://moleculer.services/docs/0.14/moleculer-web.html#Examples

參考文獻

[1] Moleculer Runner, https://moleculer.services/docs/0.14/runner.html
[2] JWT Example, https://github.com/moleculerjs/moleculer-web/blob/master/examples/full/index.js#L322
[3] HTTP ETag, https://en.wikipedia.org/wiki/HTTP_ETag
[4] Auto generate ETag & freshness check, https://github.com/moleculerjs/moleculer-web/pull/92

家家酒小劇場

  • Otter - 閘道器好像交通警察在指揮交通@@
  • Boxy - 在網路的世界中,就是用來指揮伺服器將資料送往其它伺服器,整合服務團隊合作的重要模組唷。

上一篇
Day 24 : API 閘道器 - Part 1
下一篇
Day 26 : 資料庫 Adapters
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言