Nodejs最佳實踐

項目結(jié)構(gòu)實踐

  1. 組件式構(gòu)建解決方案
    推薦: 通過組件構(gòu)建解決方案
- components
   - orders
   - users
      - index.js
      - user.js
       - userService.js

避免: 按照技術(shù)角色對文件進(jìn)行分組

- controllers
  - api.js
  - home.js
- models
  - order.js
  - user.js
  1. 分層設(shè)計組件蔓罚,保持Express在特定的區(qū)域
    每一個組件都應(yīng)該包含層級,一個專注的用于接入網(wǎng)絡(luò),邏輯追逮,數(shù)據(jù)的概念
    經(jīng)典的分層結(jié)構(gòu)霜威,將代碼分類 web, service,和DAL層谈喳。每一層次只和上一層的結(jié)構(gòu)打交道

  2. 公共使用的工具封裝成npm包
    和java的maven二方包類似

  3. 使用易于設(shè)置環(huán)境變量,安全和分級的配置
    配置文件類似egg.js 中production.config.js, dev.config.js等戈泼⌒銮荩可以通過比如rc, nconf, configconvict等幾種包完成。

錯誤處理最佳實踐

  1. 使用內(nèi)建的錯誤對象
    比如
throw new Error("not Provieded");

或者構(gòu)建自定義的錯誤對象

//從node錯誤派生的集中錯誤對象
function appError(name, httpCode, description, isOperational) {
    Error.call(this);
    Error.captureStackTrace(this);
    this.name = name;
    //...在這賦值其它屬性
};

appError.prototype = Object.create(Error.prototype);
appError.prototype.constructor = appError;

module.exports.appError = appError;
  1. 區(qū)分運(yùn)行錯誤(runtimeError)和程序設(shè)計錯誤

  2. 集中處理錯誤大猛,并且在特殊情況產(chǎn)生時候扭倾,優(yōu)雅退出服務(wù)

process.on('unhandledRejection', (reason, p) => {
  //我剛剛捕獲了一個未處理的promise rejection, 因為我們已經(jīng)有了對于未處理錯誤的后備的處理機(jī)制(見下面), 直接拋出,讓它來處理
  throw reason;
});
process.on('uncaughtException', (error) => {
  //我剛收到一個從未被處理的錯誤挽绩,現(xiàn)在處理它膛壹,并決定是否需要重啟應(yīng)用
  errorManagement.handler.handleError(error);
  if (!errorManagement.handler.isTrustedError(error))
    process.exit(1);
});

安全最佳實踐

  1. DOS攻擊,可以使用cloud負(fù)載均衡唉堪,防火墻或者nginx等做限流處理模聋,對于小的應(yīng)用,可以使用限流中間件做限速限制
const http = require('http');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');

const redisClient = redis.createClient({
 enable_offline_queue: false,
});

// Maximum 20 requests per second
const rateLimiter = new RateLimiterRedis({
 storeClient: redisClient,
 points: 20,
 duration: 1,
 blockDuration: 2, // block for 2 seconds if consumed more than 20 points per second
});

http.createServer(async (req, res) => {
  try {
  const rateLimiterRes = await rateLimiter.consume(req.socket.remoteAddress);
  // Some app logic here

  res.writeHead(200);
  res.end();
  } catch {
  res.writeHead(429);
  res.end('Too Many Requests');
  }
})
 .listen(3000);
  1. 不要在配置文件或者源代碼中存儲文本機(jī)密信息唠亚,使用環(huán)境變量或者Docker Secrets等安全管理系統(tǒng)链方。
 const azure = require('azure');

const apiKey = process.env.AZURE_STORAGE_KEY;

可以使用git commit來審計提交和提交消息,以便意外添加秘密趾撵,例如git-secrets.

  1. 使用ORM庫防止SQL注入的漏洞
  1. 通用安全最佳實踐集合
  1. 調(diào)整HTTP響應(yīng)頭加強(qiáng)安全性
    用程序應(yīng)該使用安全的header來防止攻擊者使用常見的攻擊方式占调,諸如跨站點(diǎn)腳本(XSS)、點(diǎn)擊劫持和其他惡意攻擊移剪【可海可以使用模塊,比如 helmet輕松進(jìn)行配置
    參考:
    https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/security/secureheaders.md

  2. 經(jīng)常自動檢查易受攻擊的依賴庫
    在npm的生態(tài)系統(tǒng)中, 一個項目有許多依賴是很常見的纵苛。在找到新的漏洞時, 應(yīng)始終將依賴項保留在檢查中剿涮。使用工具言津,類似于npm audit 或者 snyk跟蹤、監(jiān)視和修補(bǔ)易受攻擊的依賴項取试。將這些工具與 CI 設(shè)置集成, 以便在將其上線之前捕捉到易受攻擊的依賴庫悬槽。

  3. 避免使用Node.js的crypto庫處理密碼,使用Bcrypt
    當(dāng)存儲用戶密碼的時候瞬浓,建議使用bcrypt npm module提供的自適應(yīng)哈希算法bcrypt初婆,而不是使用Node.js的crypto模塊。由于Math.random()的可預(yù)測性猿棉,它也不應(yīng)該作為密碼或者令牌生成的一部分磅叛。

  4. 轉(zhuǎn)義 HTML、JS 和 CSS 輸出
    發(fā)送給瀏覽器的不受信任數(shù)據(jù)可能會被執(zhí)行, 而不是顯示, 這通常被稱為跨站點(diǎn)腳本(XSS)攻擊萨赁。使用專用庫將數(shù)據(jù)顯式標(biāo)記為不應(yīng)執(zhí)行的純文本內(nèi)容(例如:編碼弊琴、轉(zhuǎn)義),可以減輕這種問題杖爽。

  5. 驗證傳入的JSON schemas
    驗證傳入請求的body payload敲董,并確保其符合預(yù)期要求, 如果沒有, 則快速報錯。為了避免每個路由中繁瑣的驗證編碼, 您可以使用基于JSON的輕量級驗證架構(gòu)慰安,比如jsonschema or joi

  6. 支持黑名單的JWT
    當(dāng)使用JSON Web Tokens(例如, 通過Passport.js), 默認(rèn)情況下, 沒有任何機(jī)制可以從發(fā)出的令牌中撤消訪問權(quán)限臣缀。一旦發(fā)現(xiàn)了一些惡意用戶活動, 只要它們持有有效的標(biāo)記, 就無法阻止他們訪問系統(tǒng)。通過實現(xiàn)一個不受信任令牌的黑名單泻帮,并在每個請求上驗證精置,來減輕此問題。

  7. 限制每個用戶允許的登錄請求
    使用諸如/login,/admin之類的較高權(quán)限的路由而沒有使用速率限制锣杂。使得應(yīng)用程序面臨暴力密碼字典攻擊的風(fēng)險脂倦。可以基于請求屬性(比如IP) 或者主體參數(shù)比如電子郵件等元莫,限制允許嘗試的次數(shù)赖阻。

const maxWrongAttemptsByIPperDay = 100;
const maxConsecutiveFailsByUsernameAndIP = 10;

const limiterSlowBruteByIP = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'login_fail_ip_per_day',
  points: maxWrongAttemptsByIPperDay,
  duration: 60 * 60 * 24,
  blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});

const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'login_fail_consecutive_username_and_ip',
  points: maxConsecutiveFailsByUsernameAndIP,
  duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
  blockDuration: 60 * 60, // Block for 1 hour
});
  1. 使用非root用戶運(yùn)行Nodejs
    Node.js作為一個具有無限權(quán)限的root用戶運(yùn)行,使得攻擊者在本地計算機(jī)獲取無限制的權(quán)利踱蠢,比如(改變iptable火欧,引流到他的服務(wù)器上)
    但是有兩個常見場景需要root權(quán)限。
  • 運(yùn)行特權(quán)端口比如80端口
  • Docker容器默認(rèn)以root運(yùn)行茎截,建議使用nginx反向代理進(jìn)行轉(zhuǎn)發(fā)到Node程序苇侵,然后監(jiān)聽非特權(quán)端口。
    非root權(quán)限制作dockerfile鏡像以
FROM node:latest
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
  1. 使用反向代理或者中間件限制負(fù)載大小
    請求Body的有效負(fù)載荷越大企锌,Node單線程就越難處理榆浓,使得攻擊者沒有大量請求(DOS/DDOS)情況下,可以先讓服務(wù)器掛掉撕攒。陡鹃,可以在邊緣上(SLB,防火墻)等限制請求body的大小烘浦。
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;
}
  1. 避免JavaScript的eval聲明
    evel允許允許時指定自定義javascript代碼,不僅僅是性能問題萍鲸,還是重要的安全問題闷叉,因為惡意的js代碼可能來源于用戶的輸入,

  2. 防止惡意RegEx讓Node.js的單線程過載執(zhí)行
    正則表達(dá)式在方便的同時脊阴,給javascript應(yīng)用造成真正的威脅握侧,特別在nodejs平臺。匹配文本的用戶輸入需要大量的CPU周期來處理蹬叭。偏向第三方的驗證包藕咏,比如validator.js,而不是采用正則秽五,或者使用safe-regex來檢測有問題的正則表達(dá)式孽查。

  3. 使用變量避免模塊加載
    避免使用被指定為參數(shù)的路徑變量導(dǎo)入(requiring/importing)另一個文件, 因為該變量可能源自用戶輸入。此規(guī)則可以擴(kuò)展到一般情況下的訪問文件(例如坦喘,fs.readFile())盲再,或者包含源自用戶輸入的動態(tài)變量的其他敏感資源。

// 不安全, 因為helperPath變量可能通過用戶輸入而改變
const uploadHelpers = require(helperPath);

// 安全
const uploadHelpers = require('./helpers/upload');
  1. 在沙箱中運(yùn)行不安全的代碼

例如, 考慮一個動態(tài)框架(如 webpack), 該框架接受自定義加載器(custom loaders), 并在構(gòu)建時動態(tài)執(zhí)行這些加載器瓣铣。在存在一些惡意插件的情況下, 我們希望最大限度地減少損害, 甚至可能讓工作流成功終止 - 這需要在一個沙箱環(huán)境中運(yùn)行插件, 該環(huán)境在資源答朋、宕機(jī)和我們共享的信息方面是完全隔離的。三個主要選項可以幫助實現(xiàn)這種隔離:

  • 一個專門的子進(jìn)程 - 這提供了一個快速的信息隔離, 但要求制約子進(jìn)程, 限制其執(zhí)行時間, 并從錯誤中恢復(fù)
  • 一個基于云的無服務(wù)框架滿足所有沙盒要求棠笑,但動態(tài)部署和調(diào)用Faas方法不是本部分的內(nèi)容
  • 一些npm庫梦碗,比如sandboxvm2允許通過一行代碼執(zhí)行隔離代碼。盡管后一種選擇在簡單中獲勝, 但它提供了有限的保護(hù)蓖救。
const Sandbox = require("sandbox");
const s = new Sandbox();

s.run( "lol)hai", function( output ) {
  console.log(output);
  //output='Syntax 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'
})
  1. 隱藏客戶端的錯誤詳細(xì)信息
    盡管子進(jìn)程非常棒, 但使用它們應(yīng)該謹(jǐn)慎洪规。如果無法避免傳遞用戶輸入,就必須經(jīng)過脫敏處理循捺。 未經(jīng)脫敏處理的輸入執(zhí)行系統(tǒng)級邏輯的危險是無限的, 從遠(yuǎn)程代碼執(zhí)行到暴露敏感的系統(tǒng)數(shù)據(jù), 甚至數(shù)據(jù)丟失斩例。準(zhǔn)備工作的檢查清單可能是這樣的
  • 避免在每一種情況下的用戶輸入, 否則驗證和脫敏處理
  • 使用user/group標(biāo)識限制父進(jìn)程和子進(jìn)程的權(quán)限
  • 在隔離環(huán)境中運(yùn)行進(jìn)程, 以防止在其他準(zhǔn)備工作失敗時產(chǎn)生不必要的副作用
const { exec } = require('child_process');

...

// 例如, 以一個腳本為例, 它采用兩個參數(shù), 其中一個參數(shù)是未經(jīng)脫敏處理的用戶輸入
exec('"/path/to/test file/someScript.sh" --someOption ' + input);

// -> 想象一下, 如果用戶只是輸入'&& rm -rf --no-preserve-root /'類似的東西, 會發(fā)生什么
// 你會得到一個不想要的結(jié)果
  1. 隱藏客戶端的錯誤詳細(xì)信息
    您實現(xiàn)自己的錯誤處理邏輯與自定義錯誤對象(被許多人認(rèn)為是最佳做法)。如果這樣做, 請確保不將整個Error對象返回到客戶端, 這可能包含一些敏感的應(yīng)用程序詳細(xì)信息从橘。
    否則敏感應(yīng)用程序詳細(xì)信息(如服務(wù)器文件路徑念赶、使用中的第三方模塊和可能被攻擊者利用的應(yīng)用程序的其他內(nèi)部工作流)可能會從stack trace發(fā)現(xiàn)的信息中泄露。

  2. 避免將機(jī)密信息發(fā)布到NPM倉庫
    應(yīng)該采取預(yù)防措施來避免偶然地將機(jī)密信息發(fā)布到npm倉庫的風(fēng)險恰力。 一個 .npmignore 文件可以被用作忽略掉特定的文件或目錄, 或者一個在 package.json 中的files 數(shù)組可以起到一個白名單的作用.

一個. npmignore文件

#tests
test
coverage

#build tools
.travis.yml
.jenkins.yml

#environment
.env
.config
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叉谜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子牺勾,更是在濱河造成了極大的恐慌正罢,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驻民,死亡現(xiàn)場離奇詭異翻具,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)回还,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門裆泳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柠硕,你說我怎么就攤上這事工禾。” “怎么了蝗柔?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵闻葵,是天一觀的道長。 經(jīng)常有香客問我癣丧,道長槽畔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任胁编,我火速辦了婚禮厢钧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嬉橙。我一直安慰自己早直,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布市框。 她就那樣靜靜地躺著霞扬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枫振。 梳的紋絲不亂的頭發(fā)上喻圃,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音蒋得,去河邊找鬼级及。 笑死,一個胖子當(dāng)著我的面吹牛额衙,可吹牛的內(nèi)容都是我干的饮焦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼窍侧,長吁一口氣:“原來是場噩夢啊……” “哼县踢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伟件,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤硼啤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后斧账,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谴返,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡煞肾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗓袱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片籍救。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渠抹,靈堂內(nèi)的尸體忽然破棺而出蝙昙,到底是詐尸還是另有隱情,我是刑警寧澤梧却,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布奇颠,位于F島的核電站,受9級特大地震影響放航,放射性物質(zhì)發(fā)生泄漏烈拒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一三椿、第九天 我趴在偏房一處隱蔽的房頂上張望缺菌。 院中可真熱鬧,春花似錦搜锰、人聲如沸伴郁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焊傅。三九已至,卻和暖如春狈涮,著一層夾襖步出監(jiān)牢的瞬間狐胎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工歌馍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留握巢,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓松却,卻偏偏與公主長得像暴浦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晓锻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容