nodejs --- 融會貫通 (三)

項目管理

多環境配置

  • JSON 配置文件
  • 環境變量 使用第三方模塊管理(nconf)

依賴管理

  • dependencies:模塊正常運行須要的依賴
  • devDependencies:開發時候須要的依賴
  • optionalDependencies:非必要依賴,某種程度上加強
  • peerDependencies:運行時依賴,限定版本

異常處理

處理未捕獲的異常

除非開發者記得添加.catch語句,在這些地方拋出的錯誤都不會被 uncaughtException 事件處理程序來處理,而後消失掉。javascript

Node 應用不會奔潰,但可能致使內存泄露css

process.on('uncaughtException', (error) => {
    // 我剛收到一個從未被處理的錯誤
    // 如今處理它,並決定是否須要重啓應用
    errorManagement.handler.handleError(error);
    if (!errorManagement.handler.isTrustedError(error)) {
        process.exit(1);
    }
});

process.on('unhandledRejection', (reason, p) => {
    // 我剛剛捕獲了一個未處理的promise rejection,
    // 由於咱們已經有了對於未處理錯誤的後備的處理機制(見下面)
    // 直接拋出,讓它來處理
    throw reason;
});
複製代碼

經過 domain 管理異常

  • 經過 domain 模塊的 create 方法建立實例
  • 某個錯誤已經任何其餘錯誤都會被同一個 error 處理方法處理
  • 任何在這個回調中致使錯誤的代碼都會被 domain 覆蓋到
  • 容許咱們代碼在一個沙盒運行,而且可使用 res 對象給用戶反饋

    const domain = require('domain'); const audioDomain = domain.create();html

    audioDomain.on('error', function (err) { console.log('audioDomain error:', err); });java

    audioDomain.run(function () { const musicPlayer = new MusicPlayer(); musicPlayer.play(); });node

Joi 驗證參數

const memberSchema = Joi.object().keys({
    password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
    birthyear: Joi.number().integer().min(1900).max(2013),
    email: Joi.string().email(),
});

function addNewMember(newMember) {
    //assertions come first
    Joi.assert(newMember, memberSchema); //throws if validation fails

    //other logic here
}
複製代碼

Kibana 系統監控

看這篇文章github.com/goldbergyon…ios

上線實踐

使用 winston 記錄日記

var winston = require('winston');
var moment = require('moment');

const logger = new(winston.Logger)({
    transports: [
    new(winston.transports.Console)({
            timestamp: function () {
                return moment().format('YYYY-MM-DD HH:mm:ss')
            },
            formatter: function (params) {
                let time = params.timestamp() // 時間
                let message = params.message // 手動信息
                let meta = params.meta && Object.keys(params.meta).length ? '\n\t' + JSON.stringify(
                    params.meta) : ''
                return `${time} ${message}`
            },
        }),
    new(winston.transports.File)({
            filename: `${__dirname}/../winston/winston.log`,
            json: false,
            timestamp: function () {
                return moment().format('YYYY-MM-DD HH:mm:ss')
            },
            formatter: function (params) {
                let time = params.timestamp() // 時間
                let message = params.message // 手動信息
                let meta = params.meta && Object.keys(params.meta).length ? '\n\t' + JSON.stringify(
                    params.meta) : ''
                return `${time} ${message}`
            }
        })
  ]
})

module.exports = logger

// logger.error('error')
// logger.warm('warm')
// logger.info('info')
複製代碼

委託反向代理

Node 處理 CPU 密集型任務,如 gzipping,SSL termination 等,表現糟糕。相反,使用一個真正的中間件服務像 Nginx 更好。不然可憐的單線程 Node 將不幸地忙於處理網絡任務,而不是處理應用程序核心,性能會相應下降。nginx

雖然 express.js 經過一些 connect 中間件處理靜態文件,但你不該該使用它。Nginx 能夠更好地處理靜態文件,並能夠防止請求動態內容堵塞咱們的 node 進程。git

# 配置 gzip 壓縮
gzip on;
gzip_comp_level 6;
gzip_vary on;

# 配置 upstream
upstream myApplication {
  server 127.0.0.1:3000;
  server 127.0.0.1:3001;
  keepalive 64;
}

#定義 web server
server {
  # configure server with ssl and error pages
  listen 80;
  listen 443 ssl;
  ssl_certificate /some/location/sillyfacesociety.com.bundle.crt;
  error_page 502 /errors/502.html;

  # handling static content
  location ~ ^/(images/|img/|javascript/|js/|css/|stylesheets/|flash/|media/|static/|robots.txt|humans.txt|favicon.ico) {
  root /usr/local/silly_face_society/node/public;
  access_log off;
  expires max;
}
複製代碼

檢測有漏洞的依賴項

docs.npmjs.com/cli/auditgithub

PM2 HTTP 集羣配置

工做線程配置web

  • pm2 start app.js -i 4,-i 4 是以 cluster_mode 形式運行 app,有 4 個工做線程,若是配置 0,PM2 會根據 CPU 核心數來生成對應的工做線程
  • 工做線程掛了 PM2 會當即將其重啓 pm2 scale對集羣進行擴展

PM2 自動啓動

  • pm2 save 保存當前運行的應用
  • pm2 startup 啓動

性能實踐

避免使用 Lodash

  • 使用像 lodash 這樣的方法庫這會致使沒必要要的依賴和較慢的性能
  • 隨着新的 V8 引擎和新的 ES 標準的引入,原生方法獲得了改進,如今性能比方法庫提升了 50% 使用 ESLint 插件檢測:

    { "extends": [ "plugin:you-dont-need-lodash-underscore/compatible" ] }

benchmark

const _ = require('lodash'),
    __ = require('underscore'),
    Suite = require('benchmark').Suite,
    opts = require('./utils');
//cf. https://github.com/Berkmann18/NativeVsUtils/blob/master/utils.js

const concatSuite = new Suite('concat', opts);
const array = [0, 1, 2];

concatSuite.add('lodash', () => _.concat(array, 3, 4, 5))
    .add('underscore', () => __.concat(array, 3, 4, 5))
    .add('native', () => array.concat(3, 4, 5))
    .run({
        'async': true
    });
複製代碼

使用 prof 進行性能分析

使用 tick-processor 工具處理分析

node --prof profile-test.js

npm install tick -gnode-tick-processor
複製代碼

使用 headdump 堆快照

  • 代碼加載模塊進行快照文件生成
  • Chrome Profiles 加載快照文件

    yarn add heapdump -D

    const heapdump = require('heapdump'); const string = '1 string to rule them all';

    const leakyArr = []; let count = 2; setInterval(function () { leakyArr.push(string.replace(/1/g, count++)); }, 0);

    setInterval(function () { if (heapdump.writeSnapshot()) console.log('wrote snapshot'); }, 20000);

應用安全清單

helmet 設置安全響應頭

檢測頭部配置:Security Headers

應用程序應該使用安全的 header 來防止攻擊者使用常見的攻擊方式,諸如跨站點腳本攻擊(XSS)、跨站請求僞造(CSRF)。可使用模塊 helmet 輕鬆進行配置。

  • 構造: X-Frame-Options:sameorigin。提供點擊劫持保護,iframe 只能同源。

  • 傳輸:Strict-Transport-Security:max-age=31536000; includeSubDomains。強制 HTTPS,這減小了web 應用程序中錯誤經過 cookies 和外部連接,泄露會話數據,並防止中間人攻擊

  • 內容:X-Content-Type-Options:nosniff。阻止從聲明的內容類型中嗅探響應,減小了用戶上傳惡意內容形成的風險 Content-Type:text/html;charset=utf-8。指示瀏覽器將頁面解釋爲特定的內容類型,而不是依賴瀏覽器進行假設

  • XSS:X-XSS-Protection:1; mode=block。啓用了內置於最新 web 瀏覽器中的跨站點腳本(XSS)過濾器

  • 下載:X-Download-Options:noopen。

  • 緩存:Cache-Control:no-cache。web 應中返回的數據能夠由用戶瀏覽器以及中間代理緩存。該指令指示他們不要保留頁面內容,以避免其餘人從這些緩存中訪問敏感內容 Pragma:no-cache。同上 Expires:-1。web 響應中返回的數據能夠由用戶瀏覽器以及中間代理緩存。該指令經過將到期時間設置爲一個值來防止這種狀況。

  • 訪問控制:Access-Control-Allow-Origin:not *。'Access-Control-Allow-Origin: *' 默認在現代瀏覽器中禁用 X-Permitted-Cross-Domain-Policies:master-only。指示只有指定的文件在此域中才被視爲有效

= 內容安全策略:Content-Security-Policy:內容安全策略須要仔細調整並精肯定義策略

  • 服務器信息:Server:不顯示。

使用 security-linter 插件

使用安全檢驗插件 eslint-plugin-security 或者 tslint-config-security。

koa-ratelimit 限制併發請求

DOS 攻擊很是流行並且相對容易處理。使用外部服務,好比 cloud 負載均衡, cloud 防火牆, nginx, 或者(對於小的,不是那麼重要的app)一個速率限制中間件(好比 koa-ratelimit),來實現速率限制。

純文本機密信息放置

存儲在源代碼管理中的機密信息必須進行加密和管理 (滾動密鑰(rolling keys)、過時時間、審覈等)。使用 pre-commit/push 鉤子防止意外提交機密信息。

ORM/ODM 庫防止查詢注入漏洞

要防止 SQL/NoSQL 注入和其餘惡意攻擊, 請始終使用 ORM/ODM 或 database 庫來轉義數據或支持命名的或索引的參數化查詢, 並注意驗證用戶輸入的預期類型。不要只使用 JavaScript 模板字符串或字符串串聯將值插入到查詢語句中, 由於這會將應用程序置於普遍的漏洞中。

庫:

  • TypeORM
  • sequelize
  • mongoose
  • Knex
  • Objection.js
  • waterline

使用 Bcrypt 代替 Crypto

密碼或機密信息(API 密鑰)應該使用安全的 hash + salt 函數(bcrypt)來存儲, 由於性能和安全緣由, 這應該是其 JavaScript 實現的首選。

// 使用10個哈希回合異步生成安全密碼
bcrypt.hash('myPassword', 10, function (err, hash) {
    // 在用戶記錄中存儲安全哈希
});

// 將提供的密碼輸入與已保存的哈希進行比較
bcrypt.compare('somePassword', hash, function (err, match) {
    if (match) {
        // 密碼匹配
    } else {
        // 密碼不匹配
    }
});
複製代碼

轉義 HTML、JS 和 CSS 輸出

發送給瀏覽器的不受信任數據可能會被執行, 而不是顯示, 這一般被稱爲跨站點腳本(XSS)攻擊。使用專用庫將數據顯式標記爲不該執行的純文本內容(例如:編碼、轉義),能夠減輕這種問題。

驗證傳入的 JSON schemas

驗證傳入請求的 body payload,並確保其符合預期要求, 若是沒有, 則快速報錯。爲了不每一個路由中繁瑣的驗證編碼, 您可使用基於 JSON 的輕量級驗證架構,好比 jsonschema 或 joi

支持黑名單的 JWT

當使用 JSON Web Tokens(例如, 經過 Passport.js), 默認狀況下, 沒有任何機制能夠從發出的令牌中撤消訪問權限。一旦發現了一些惡意用戶活動, 只要它們持有有效的標記, 就沒法阻止他們訪問系統。經過實現一個不受信任令牌的黑名單,並在每一個請求上驗證,來減輕此問題。

const jwt = require('express-jwt');
const blacklist = require('express-jwt-blacklist');

app.use(jwt({
    secret: 'my-secret',
    isRevoked: blacklist.isRevoked
}));

app.get('/logout', function (req, res) {
    blacklist.revoke(req.user)
    res.sendStatus(200);
});
複製代碼

限制每一個用戶容許的登陸請求

一類保護暴力破解的中間件,好比 express-brute,應該被用在 express 的應用中,來防止暴力/字典攻擊;這類攻擊主要應用於一些敏感路由,好比 /admin 或者 /login,基於某些請求屬性, 如用戶名, 或其餘標識符, 如正文參數等。不然攻擊者能夠發出無限制的密碼匹配嘗試, 以獲取對應用程序中特權賬戶的訪問權限。

const ExpressBrute = require('express-brute');
const RedisStore = require('express-brute-redis');

const redisStore = new RedisStore({
    host: '127.0.0.1',
    port: 6379
});

// Start slowing requests after 5 failed 
// attempts to login for the same user
const loginBruteforce = new ExpressBrute(redisStore, {
    freeRetries: 5,
    minWait: 5 * 60 * 1000, // 5 minutes
    maxWait: 60 * 60 * 1000, // 1 hour
    failCallback: failCallback,
    handleStoreError: handleStoreErrorCallback
});

app.post('/login',
    loginBruteforce.getMiddleware({
        key: function (req, res, next) {
            // prevent too many attempts for the same username
            next(req.body.username);
        }
    }), // error 403 if we hit this route too often
    function (req, res, next) {
        if (User.isValidLogin(req.body.username, req.body.password)) {
            // reset the failure counter for valid login
            req.brute.reset(function () {
                res.redirect('/'); // logged in
            });
        } else {
            // handle invalid user
        }
    }
);
複製代碼

使用非 root 用戶運行 Node.js

Node.js 做爲一個具備無限權限的 root 用戶運行,這是一種廣泛的情景。例如,在 Docker 容器中,這是默認行爲。建議建立一個非 root 用戶,並保存到 Docker 鏡像中(下面給出了示例),或者經過調用帶有"-u username" 的容器來表明此用戶運行該進程。不然在服務器上運行腳本的攻擊者在本地計算機上得到無限制的權利 (例如,改變 iptable,引流到他的服務器上)

FROM node:latestCOPY package.json .RUN npm installCOPY . .EXPOSE 3000USER nodeCMD ["node", "server.js"]
複製代碼

使用反向代理或中間件限制負載大小

請求 body 有效載荷越大, Node.js 的單線程就越難處理它。這是攻擊者在沒有大量請求(DOS/DDOS 攻擊)的狀況下,就可讓服務器跪下的機會。在邊緣上(例如,防火牆,ELB)限制傳入請求的 body 大小,或者經過配置 express body parser 僅接收小的載荷,能夠減輕這種問題。不然您的應用程序將不得不處理大的請求, 沒法處理它必須完成的其餘重要工做, 從而致使對 DOS 攻擊的性能影響和脆弱性。

express

const express = require('express');
const app = express();
// body-parser defaults to a body size limit of 300kb
app.use(express.json({
    limit: '300kb'
}));

// Request with json body
app.post('/json', (req, res) => {

    // Check if request payload content-type matches json
    // because body-parser does not check for content types
    if (!req.is('json')) {
        return res.sendStatus(415); // Unsupported media type if request doesn't have JSON body
    }

    res.send('Hooray, it worked!');
});

app.listen(3000, () => console.log('Example app listening on port 3000!'));
複製代碼

nginx:

http {
    ...
    # Limit the body size for ALL incoming requests to 1 MB
    client_max_body_size 1m;
}

server {
    ...
    # Limit the body size for incoming requests to this specific server block to 1 MB
    client_max_body_size 1m;
}

location /upload {
    ...
    # Limit the body size for incoming requests to this route to 1 MB
    client_max_body_size 1m;
}
複製代碼

防止 RegEx 讓 NodeJS 過載

匹配文本的用戶輸入須要大量的 CPU 週期來處理。在某種程度上,正則處理是效率低下的,好比驗證 10 個單詞的單個請求可能阻止整個 event loop 長達6秒。因爲這個緣由,偏向第三方的驗證包,好比validator.js,而不是採用正則,或者使用 safe-regex 來檢測有問題的正則表達式。

const saferegex = require('safe-regex');
const emailRegex =
    /^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/;

// should output false because the emailRegex is vulnerable to redos attacks
console.log(saferegex(emailRegex));

// instead of the regex pattern, use validator:
const validator = require('validator');
console.log(validator.isEmail('liran.tal@gmail.com'));
複製代碼

在沙箱中運行不安全代碼

當任務執行在運行時給出的外部代碼時(例如, 插件), 使用任何類型的沙盒執行環境保護主代碼,並隔離開主代碼和插件。這能夠經過一個專用的過程來實現 (例如:cluster.fork()), 無服務器環境或充當沙盒的專用 npm 包。

  • 一個專門的子進程 - 這提供了一個快速的信息隔離, 但要求制約子進程, 限制其執行時間, 並從錯誤中恢復
  • 一個基於雲的無服務框架知足全部沙盒要求,但動態部署和調用Faas方法不是本部分的內容
  • 一些 npm 庫,好比 sandbox 和 vm2 容許經過一行代碼執行隔離代碼。儘管後一種選擇在簡單中獲勝, 但它提供了有限的保護。

    const Sandbox = require("sandbox"); const s = new Sandbox();

    s.run("lol)hai", function (output) { console.log(output); //output='Synatx error' });

    // Example 4 - Restricted code s.run("process.platform", function (output) { console.log(output); //output=Null })

    // Example 5 - Infinite loop s.run("while (true) {}", function (output) { console.log(output); //output='Timeout' })

隱藏客戶端的錯誤詳細信息

默認狀況下, 集成的 express 錯誤處理程序隱藏錯誤詳細信息。可是, 極有可能, 您實現本身的錯誤處理邏輯與自定義錯誤對象(被許多人認爲是最佳作法)。若是這樣作, 請確保不將整個 Error 對象返回到客戶端, 這可能包含一些敏感的應用程序詳細信息。不然敏感應用程序詳細信息(如服務器文件路徑、使用中的第三方模塊和可能被攻擊者利用的應用程序的其餘內部工做流)可能會從 stack trace 發現的信息中泄露。

// production error handler
 // no stacktraces leaked to user
 app.use(function (err, req, res, next) {
     res.status(err.status || 500);
     res.render('error', {
         message: err.message,
         error: {}
     });
 });
複製代碼

對 npm 或 Yarn,配置 2FA

開發鏈中的任何步驟都應使用 MFA(多重身份驗證)進行保護, npm/Yarn 對於那些可以掌握某些開發人員密碼的攻擊者來講是一個很好的機會。使用開發人員憑據, 攻擊者能夠向跨項目和服務普遍安裝的庫中注入惡意代碼。甚至可能在網絡上公開發布。在 npm 中啓用兩層身份驗證(2-factor-authentication), 攻擊者幾乎沒有機會改變您的軟件包代碼。

session 中間件設置

每一個 web 框架和技術都有其已知的弱點,告訴攻擊者咱們使用的 web 框架對他們來講是很大的幫助。使用 session 中間件的默認設置, 能夠以相似於 X-Powered-Byheader 的方式向模塊和框架特定的劫持攻擊公開您的應用。嘗試隱藏識別和揭露技術棧的任何內容(例如:Nonde.js, express)。不然能夠經過不安全的鏈接發送cookie, 攻擊者可能會使用會話標識來標識web應用程序的基礎框架以及特定於模塊的漏洞。

// using the express session middleware
app.use(session({
    secret: 'youruniquesecret', // secret string used in the signing of the session ID that is stored in the cookie
    name: 'youruniquename', // set a unique name to remove the default connect.sid
    cookie: {
        httpOnly: true, // minimize risk of XSS attacks by restricting the client from reading the cookie
        secure: true, // only send cookie over https
        maxAge: 60000 * 60 * 24 // set cookie expiry length in ms
    }
}));
複製代碼

csurf 防止 CSRF

路由層:

var cookieParser = require('cookie-parser');
var csrf = require('csurf');
var bodyParser = require('body-parser');
var express = require('express');

// 設置路由中間件
var csrfProtection = csrf({
    cookie: true
});
var parseForm = bodyParser.urlencoded({
    extended: false
});

var app = express();

// 咱們須要這個,由於在 csrfProtection 中 「cookie」 是正確的
app.use(cookieParser());

app.get('/form', csrfProtection, function (req, res) {
    // 將 CSRFToken 傳遞給視圖
    res.render('send', {
        csrfToken: req.csrfToken()
    });
});

app.post('/process', parseForm, csrfProtection, function (req, res) {
    res.send('data is being processed');
});
複製代碼

展現層:

<form action="/process" method="POST">    <input type="hidden" name="_csrf" value="{{csrfToken}}">  Favorite color: <input type="text" name="favoriteColor">  <button type="submit">Submit</button></form>  
複製代碼

綜合應用

watch 服務

const fs = require('fs');
const exec = require('child_process').exec;

function watch() {
    const child = exec('node server.js');
    const watcher = fs.watch(__dirname + '/server.js', function () {
        console.log('File changed, reloading.');
        child.kill();
        watcher.close();
        watch();
    });
}

watch();
複製代碼

RESTful web 應用

  • REST 意思是表徵性狀態傳輸
  • 使用正確的 HTTP 方法、URLs 和頭部信息來建立語義化 RESTful API
  • GET /gages:獲取
  • POST /pages:建立
  • GET /pages/10:獲取 pages10
  • PATCH /pages/10:更新 pages10
  • PUT /pages/10:替換 pages10
  • DELETE /pages/10:刪除 pages10

    let app; const express = require('express'); const routes = require('./routes');

    module.exports = app = express();

    app.use(express.json()); // 使用 JSON body 解析 app.use(express.methodOverride()); // 容許一個查詢參數來制定額外的 HTTP 方法

    // 資源使用的路由 app.get('/pages', routes.pages.index); app.get('/pages/:id', routes.pages.show); app.post('/pages', routes.pages.create); app.patch('/pages/:id', routes.pages.patch); app.put('/pages/:id', routes.pages.update); app.del('/pages/:id', routes.pages.remove);

中間件應用

const express = require('express');
const app = express();
const Schema = require('validate');
const xml2json = require('xml2json');
const util = require('util');
const Page = new Schema();

Page.path('title').type('string').required(); // 數據校驗確保頁面有標題

function ValidatorError(errors) { // 從錯誤對象繼承,校驗出現的錯誤在錯誤中間件處理
    this.statusCode = 400;
    this.message = errors.join(', ');
}
util.inherits(ValidatorError, Error);

function xmlMiddleware(req, res, next) { // 處理 xml 的中間件
    if (!req.is('xml')) return next();

    let body = '';
    req.on('data', function (str) { // 從客戶端讀到數據時觸發
        body += str;
    });

    req.on('end', function () {
        req.body = xml2json.toJson(body.toString(), {
            object: true,
            sanitize: false,
        });
        next();
    });
}

function checkValidXml(req, res, next) { // 數據校驗中間件
    const page = Page.validate(req.body.page);
    if (page.errors.length) {
        next(new ValidatorError(page.errors)); // 傳遞錯誤給 next 阻止路由繼續運行
    } else {
        next();
    }
}

function errorHandler(err, req, res, next) { // 錯誤處理中間件
    console.error('errorHandler', err);
    res.send(err.statusCode || 500, err.message);
}

app.use(xmlMiddleware); // 應用 XML 中間件到全部的請求中

app.post('/pages', checkValidXml, function (req, res) { // 特定的請求校驗 xml
    console.log('Valid page:', req.body.page);
    res.send(req.body);
});

app.use(errorHandler); // 添加錯誤處理中間件

app.listen(3000);
複製代碼

經過事件組織應用

// 監聽用戶註冊成功消息,綁定郵件程序
const express = require('express');
const app = express();
const emails = require('./emails');
const routes = require('./routes');

app.use(express.json());

app.post('/users', routes.users.create); // 設置路由建立用戶

app.on('user:created', emails.welcome); // 監聽建立成功事件,綁定 email 代碼

module.exports = app;

// 用戶註冊成功發起事件
const User = require('./../models/user');

module.exports.create = function (req, res, next) {
    const user = new User(req.body);
    user.save(function (err) {
        if (err) return next(err);
        res.app.emit('user:created', user); // 當用戶成功註冊時觸發建立用戶事件
        res.send('User created');
    });
};
複製代碼

WebSocket 與 session

const express = require('express');
const WebSocketServer = require('ws').Server;
const parseCookie = express.cookieParser('some secret'); // 加載解析 cookie 中間件,設置密碼
const MemoryStore = express.session.MemoryStore; // 加載要使用的會話存儲
const store = new MemoryStore();

const app = express();
const server = app.listen(process.env.PORT || 3000);

app.use(parseCookie);
app.use(express.session({
    store: store,
    secret: 'some secret'
})); // 告知 Express 使用會話存儲和設置密碼(使用 session 中間件)
app.use(express.static(__dirname + '/public'));

app.get('/random', function (req, res) { // 測試測試用的會話值
    req.session.random = Math.random().toString();
    res.send(200);
});

// 設置 WebSocket 服務器,將其傳遞給 Express 服務器
// 須要傳遞已有的 Express 服務(listen 的返回對象)
const webSocketServer = new WebSocketServer({
    server: server
});

// 在鏈接事件給客戶端建立 WebSocket
webSocketServer.on('connection', function (ws) {
    let session;

    ws.on('message', function (data, flags) {
        const message = JSON.parse(data);

        // 客戶端發送的 JSON,須要一些代碼來解析 JSON 字符串肯定是否可用
        if (message.type === 'getSession') {
            parseCookie(ws.upgradeReq, null, function (err) {
                // 從 HTTP 的更新請求中獲取 WebSocket 的會話 ID
                // 一旦 WebSockets 服務器有一個鏈接,session ID 能夠用=從初始化請求中的 cookies 中獲取
                const sid = ws.upgradeReq.signedCookies['connect.sid'];

                // 從存儲中獲取用戶的會話信息
                // 只須要在初始化的請求中傳遞一個引用給解析 cookie 的中間件
                // 而後 session 可使用 session 存儲的 get 方法加載
                store.get(sid, function (err, loadedSession) {
                    if (err) console.error(err);
                    session = loadedSession;
                    ws.send('session.random: ' + session.random, {
                        mask: false,
                    }); // session 加載後會把一個包含了 session 值的消息發回給客戶端
                });
            });
        } else {
            ws.send('Unknown command');
        }
    });
});

<!DOCTYPE html>
<html>
	<head>
		<script>
    const host = window.document.location.host.replace(/:.*/, '');
    const ws = new WebSocket('ws://' + host + ':3000');

    setInterval(function () {
      ws.send('{ "type": "getSession" }'); // 按期向服務器發送消息
    }, 1000);

    ws.onmessage = function (event) {
      document.getElementById('message').innerHTML = event.data;
    };
  </script>
	</head>
	<body>
		<h1>WebSocket sessions</h1>
		<div id='message'></div>
		<br>
		</body>
	</html>
複製代碼

Express4 中間件

package 描述
body-parser 解析 URL 編碼 和 JSON POST 請求的 body 數據
compression 壓縮服務器響應
connect-timeout 請求容許超時
cookie-parser 從 HTTP 頭部信息中解析 cookies,結果放在 req.cookies
cookie-session 使用 cookies 來支持簡單會話
csurf 在會話中添加 token,防護 CSRF 攻擊
errorhandler Connect 中使用的默認錯誤處理
express-session 簡單的會話處理,使用 stores 擴展來吧會話信息寫入到數據庫或文件中
method-override 映射新的 HTTP 動詞到請求變量中的 _method
morgan 日誌格式化
response-time 跟蹤響應時間
serve-favicon 發送網站圖標
serve-index 目錄列表
whost 容許路由匹配子域名

JWT

JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。

跨域認證

通常流程

  • 用戶向服務器發送用戶名和密碼
  • 服務器驗證經過後,在當前對話(session)裏面保存相關數據,好比用戶角色、登陸時間等等
  • 服務器向用戶返回一個 session_id,寫入用戶的 Cookie
  • 用戶隨後的每一次請求,都會經過 Cookie,將 session_id 傳回服務器
  • 服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份

session 共享

在服務器集羣,要求 session 數據共享,每臺服務器都可以讀取 session:

  • 一種解決方案是 session 數據持久化,寫入數據庫或別的持久層。各類服務收到請求後,都向持久層請求數據。這種方案的優勢是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。
  • 另外一種方案是服務器索性不保存 session 數據了,全部數據都保存在客戶端,每次請求都發回服務器。JWT 就是這種方案的一個表明。

JWT

原理

  • 服務器認證之後,生成一個 JSON 對象,發回給用戶
  • 用戶與服務端通訊的時候,都要發回這個 JSON 對象,服務器徹底只靠這個對象認定用戶身份
  • 防止篡改會加上簽名

數據結構

Header(頭部).Payload(負載).Signature(簽名):

  • Header:JSON,使用 Base64 URL 轉成字符串
  • Payload:JSON,使用 Base64 URL 轉成字符串
  • Signature:對前兩部分的簽名

Header

{  "alg": "HS256", // 簽名的算法  "typ": "JWT" // token 的類型}
複製代碼

Payload

{
  // 7 個官方字段
  "iss": "簽發人",
  "exp": "過時時間",
  "sub": "主題",
  "aud": "受衆",
  "nbf": "生效時間",
  "iat": "簽發時間",
  "jti": "編號",
  // 定義私有字段
  "name": "Chenng" 
}
複製代碼

Signature

HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret) # secret 祕鑰只有服務器知道
複製代碼

使用方式

  • JWT 不只能夠用於認證,也能夠用於交換信息。有效使用 JWT,能夠下降服務器查詢數據庫的次數
  • JWT 的最大缺點是,因爲服務器不保存 session 狀態,所以沒法在使用過程當中廢止某個 token,或者更改 token 的權限。也就是說,一旦 - JWT 簽發了,在到期以前就會始終有效,除非服務器部署額外的邏輯
  • JWT 自己包含了認證信息,一旦泄露,任何人均可以得到該令牌的全部權限。爲了減小盜用,JWT 的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證

koa

核心對象

  • HTTP 接收 解析 響應
  • 中間件 執行上下文
  • Koa 中一切的流程都是中間件

源碼組成

  • application
  • context
  • request
  • response

中間件的使用

const Koa = require('koa');

const app = new Koa();

const mid1 = async (ctx, next) => {
    ctx.body = 'Hi';
    await next(); // next 執行下一個中間件
    ctx.body += ' there';
};
const mid2 = async (ctx, next) => {
    ctx.type = 'text/html; chartset=utf-8';
    await next();
};
const mid3 = async (ctx, next) => {
    ctx.body += ' chenng';
    await next();
};

app.use(mid1);
app.use(mid2);
app.use(mid3);

app.listen(2333);
// Hi chenng there
複製代碼

返回媒體資源

router
    .get('/api/dynamic_image/codewars', async (ctx, next) => {
        const res = await axios.get('https://www.codewars.com/users/ringcrl');
        const [, kyu, score] = res.data
            .match(/<div class="stat"><b>Rank:<\/b>(.+?)<\/div><div class="stat"><b>Honor:<\/b>(.+?)<\/div>/);
        const svg =
            `
      <svg xmlns="http://www.w3.org/2000/svg" width="80" height="20">
        <rect x="0" y="0" width="80" height="20" fill="#fff" stroke-width="2" stroke="#cccccc"></rect>
        <rect x="0" y="0" width="50" height="20" fill="#5b5b5b"></rect>
        <text x="5" y="15" class="small" fill="#fff" style="font-size: 14px;">${kyu}</text>
        <rect x="50" y="0" width="30" height="20" fill="#3275b0"></rect>
        <text x="53" y="15" class="small" fill="#fff" style="font-size: 14px">${score}</text>
      </svg>
    `;
        ctx.set('Content-Type', 'image/svg+xml');
        ctx.body = Buffer.from(svg);
        await next();
    });
複製代碼

Web API 設計

需求

  • 易於使用
  • 便於修改
  • 健壯性好
  • 不怕公之於衆

重要準則

  • 設計容易記憶、功能一目瞭然
  • 使用合適的 HTTP 方法
  • 選擇合適的英語單詞,注意單詞的單複數形式
  • 使用 OAuth 2.0 進行認證

API 通用資源網站 ProgrammableWeb(www.programmableweb.com)中有各類已經公開的 Web API 文檔,多觀察一下

公鑰加密私鑰解密

生成公鑰私鑰

利用 openssl 生成公鑰私鑰 生成公鑰:openssl genrsa -out rsa_private_key.pem 1024 生成私鑰:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
複製代碼

crypto 使用

const crypto = require('crypto');
const fs = require('fs');

const publicKey = fs.readFileSync(`${__dirname}/rsa_public_key.pem`).toString('ascii');
const privateKey = fs.readFileSync(`${__dirname}/rsa_private_key.pem`).toString('ascii');
console.log(publicKey);
console.log(privateKey);
const data = 'Chenng';
console.log('content: ', data);

//公鑰加密
const encodeData = crypto.publicEncrypt(
    publicKey,
    Buffer.from(data),
).toString('base64');
console.log('encode: ', encodeData);

//私鑰解密
const decodeData = crypto.privateDecrypt(
    privateKey,
    Buffer.from(encodeData, 'base64'),
);
console.log('decode: ', decodeData.toString());
複製代碼

redis 緩存接口

  • 部分不用實時更新的數據使用 redis 進行緩存
  • 使用 node-schedule 在每晚定時調用接口 redis 使用

    const redis = require('redis'); const redisClient = redis.createClient(); const getAsync = promisify(redisClient.get).bind(redisClient);

    let codewarsRes = JSON.parse(await getAsync('codewarsRes')); if (!codewarsRes) { const res = await axios.get('www.codewars.com/users/ringc…'); codewarsRes = res.data; redisClient.set('codewarsRes', JSON.stringify(codewarsRes), 'EX', 86000); }

node-schedule 使用

const schedule = require('node-schedule');
const axios = require('axios');

schedule.scheduleJob('* 23 59 * *', function () {
    axios.get('https://static.chenng.cn/api/dynamic_image/leetcode_problems');
    axios.get('https://static.chenng.cn/api/dynamic_image/leetcode');
    axios.get('https://static.chenng.cn/api/dynamic_image/codewars');
});
複製代碼

參考地址:

  • https://juejin.cn/post/6844903775937757192?utm_source=gold_browser_extension
  • https://github.com/goldbergyoni/nodebestpractices
  • 《Node.js硬實戰:115個核心技巧》
相關文章
相關標籤/搜索