nodejs best pracities
https://github.com/goldbergyoni/nodebestpractices/blob/master/README.chinese.md

nodejs应用场景

  1. 基本用法 → 开发项目后端系统
  2. RestFul API服务
  3. BFF 服务端,支撑大规模线上业务
  1. 项目结构
    1. 技术选型
    2. 知识点
  2. 异常处理,错误处理
  3. 代码规范
  4. 质量保证
  5. 测试和质量实践
  6. 安全验证
  7. 上线发布,线上环境
  8. 监控
  9. 高可用
  10. 优雅退出 graceful exit
  11. pm2 nodejs 进程管理与优雅退出
  12. health check 健康检查

项目结构

  1. 应用分层,分层思维,MVC
  2. 组件化,抽离中间件
    1. 封装公共模块为 npm
  3. 抽离 utils,config
  4. 区分 app, www目录
  5. 使用环境变量,用 NODE_ENV区分环境变量
    1. cross-env 跨平台设置 NODE_ENV

分层思维

  • routes
  • controller
  • cache
  • services
  • dbms
  • views

项目配置

config https://www.npmjs.com/package/config

技术选型

  • koa2 & eggjs
  • mysql & sequelize
  • redis & session
  • jwt
  • nunjucks
  • eslint

框架
存储
缓存
用户认证

技术方案设计

  1. 架构设计
  2. API接口 & 路由设计 & 模板设计
  3. 数据表 & 数据模型设计

功能模块开发

模块化

错误处理

  1. 规范错误数据
    1. 错误码
    2. 错误信息
    3. 全局错误信息
  2. 统一错误输出
    1. 404
    2. 403
    3. 401
    4. 500
    5. 502
  3. 对输入的数据进行 schema验证

使用规范的 promise库或async-await处理 异步错误

  • 使用 return和 throw来控制程序流程
  • 替代 try catch,这会使其像try-catch一样更加简洁
  • 在任何 Promise中发生错误或异常, 都由单个.catch()处理程序处理

https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/errorhandling/asyncerrorhandling.chinese.md

Node.js回调特性, function(err, response), 是导致不可维护代码的一个必然的方式。
究其原因,是由于混合了随意的错误处理代码,臃肿的内嵌,蹩脚的代码模式
避免更多的 callback

集中处理错误,不要在中间件中处理错误

stack trace 堆栈轨迹,某个时间的调用堆栈状态
错误处理的逻辑不放在一起将会导致代码重复和非常可能不恰当的错误处理
使用Node.js的内置错误对象有助于在你的代码和第三方库之间保持一致性,它还保留了重要错误信息

错误处理中间件

在 DAO层, 不处理错误
单独处理每个错误将导致大量的重复

  • 单独处理每个错误将导致大量的代码重复。
  • 将所有错误处理逻辑委派给一个express & koa中间件
    1. // 错误处理中间件,我们委托集中式错误处理程序处理错误
    2. app.use(function (err, req, res, next) {
    3. errorHandler.handleError(err).then((isOperationalError) => {
    4. if (!isOperationalError)
    5. next(err);
    6. });
    7. });

Promise代替 try catch

推荐的错误处理

  1. doWork()
  2. .then(doWork)
  3. .then(doOtherWork)
  4. .then((result) => doWork)
  5. .catch((error) => {throw error;})
  6. .then(verify);

不推荐的灰调处理异常

  1. getData(someParameter, function(err, result){
  2. if(err != null)
  3. //做一些事情类似于调用给定的回调函数并传递错误
  4. getMoreData(a, function(err, result){
  5. if(err != null)
  6. //做一些事情类似于调用给定的回调函数并传递错误
  7. getMoreData(b, function(c){
  8. getMoreData(d, function(e){
  9. if(err != null)
  10. //你有什么想法? 
  11. });
  12. });

区分运行异常和代码异常

  1. 处理所有可能的错误,从不崩溃,例如:
    1. 500错误也返回正常的响应,在 response里面抛出 500的错误信息
  2. 让应用崩溃并重启

代码规范

  1. 使用 eslint强制 pre-commit
  2. 使用 jsdoc注释文件和函数
  3. 使用 async & await编写异步逻辑
  4. 规范 git分支和 commit格式

质量保证

jest单元测试

原生JS优先

https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore

使用像lodash和underscore这样的实用库替代原生的JS方法,通常来说这么做更不好,
导致了一些不必要的依赖项以及更差的性能表现。
随着新的V8引擎以及新的ES标准的引入,原生方法得到了改进,它们现在会比这些实用工具库高出大概 50% 的性能

不要阻塞事件循环

  • 避免执行CPU密集型的任务,并将这些任务转移到基于上下文的专用线程中,因为它们会阻塞大多数单线程事件循环
  • 由于事件循环被阻塞了,Node.js 将无法处理其它请求,从而导致同时请求的用户的延迟。
  • 3000 位用户正在等待响应,内容本身已经准备好了提供服务, 但是一个单独的请求阻止了服务器将结果分发回去。

少用 sleep

  1. function sleep (ms) {
  2. const future = Date.now() + ms
  3. while (Date.now() < future);
  4. }
  5. server.get('/', (req, res, next) => {
  6. sleep(30)
  7. res.send({})
  8. next()
  9. })

项目安全

  1. 处理 xss
  2. 使用 ORM防止 SQL注入
  3. rsa加密敏感信息,例如 token,密码

安全的 headers头设置

支持黑名单的JWT

一旦发现了一些恶意用户活动, 只要它们持有有效的标记, 就无法阻止他们访问系统。
通过实现一个不受信任令牌的黑名单,并在每个请求上验证,来减轻此问题

https://github.com/jaredhanson/passport

避免使用Node.js的crypto库处理密码,使用Bcrypt

把机密信息从配置文件中抽离出来,或者使用包对其加密

Bcrypt提供的自适应哈希算法bcrypt,而不是使用Node.js的crypto模块
https://www.npmjs.com/package/bcrypt

使用中间件限制并发请求

速率限制中间件(比如express-rate-limit),来实现速率限制

npm包更新

npm audit或者snyk跟踪、监视和修补易受攻击的依赖包
自动检测漏洞的工具

避免将机密信息发布到NPM仓库

.npmignore文件可以被用作忽略掉特定的文件或目录,
或者一个在package.json中的files数组可以起到一个白名单的作用

  • 采取预防措施来避免偶然地将机密信息发布到npm仓库的风险
  • 项目的API密钥、密码或者其它机密信息很容易被任何碰到的人滥用,这可能会导致经济损失、身份冒充

.npmignore file & .gitignore

  1. # Tests
  2. test
  3. coverage
  4. # Build tools
  5. .travis.yml
  6. .jenkins.yml
  7. # Environment
  8. .env
  9. .config

避免不安全的重定向

验证用户输入的重定向

  • 不验证域名的重定向可使攻击者启动网络钓鱼诈骗,窃取用户凭据

session中间件设置

httpOnly
maxAge
expires
secure

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

MFA 多重身份验证

Multi-Factor Authentication (MFA)
是一种简单有效的最佳安全实践方法,它能够在用户名和密码之外再额外增加一层安全保护。

虚拟 MFA 设备是能产生 6 位数字认证码的应用程序,遵循基于时间的一次性密码 (TOTP)标准(RFC 6238
MFA参考 https://helpcdn.aliyun.com/knowledge_detail/37215.html

npm配置 2FA

在npm中启用2因素身份验证2-factor-authentication,攻击者几乎没有机会改变您的软件包代码

隐藏客户端的错误详细信息

沙箱环境运行测试代码

sandboxvm2允许通过一行代码执行隔离代码。尽管后一种选择在简单中获胜, 但它提供了有限的保护

  1. const Sandbox = require("sandbox");
  2. const s = new Sandbox();
  3. s.run( "lol)hai", function( output ) {
  4. console.log(output);
  5. //output='Syntax error'
  6. });
  7. // Example 4 - Restricted code
  8. s.run( "process.platform", function( output ) {
  9. console.log(output);
  10. //output=Null
  11. })
  12. // Example 5 - Infinite loop
  13. s.run( "while (true) {}", function( output ) {
  14. console.log(output);
  15. //output='Timeout'
  16. })

转义 HTML、JS 和 CSS 输出

线上运维

  1. 记录日志,一定要做的
  2. 多进程
  3. 进程守护
  4. nginx代理
  5. 拆分服务,例如 mysql, redis等
  6. 系统监控,APM
  • 日志透明度
    • 从第1天计划您的日志平台:如何收集、存储和分析日志,以确保所需信息(例如,
    • 错误率、通过服务和服务器等完成整个事务)都能被提取出来
  • 委托可能的一切(例如:gzip,SSL)给反向代理,不要处理 CPU密集的任务
    • Node处理CPU密集型任务,如gzipping,SSL termination等,表现糟糕。
    • 使用一个 ‘真正’ 的中间件服务像Nginx,HAProxy或者云供应商的服务
  • package-lock.json锁定包依赖
  • 利用 CPU多核

日志系统

使用成熟的logger提高错误可见性

  1. 为每条日志添加时间戳。这条很好自我解释-你应该能够告知每个日志条目发生在什么时候。
  2. 日志格式应易于被人类和机器理解。
  3. 允许多个可配置的目标流。例如, 您可能正在将trace log写入到一个文件中, 但遇到错误时, 请写入同一文件, 然后写入到错误日志文件,并同时发送电子邮件

在每一个请求的每一条log入口,指明同一个标识符,transaction-id: {某些值}。然后在检查日志中的错误时,很容易总结出前后发生的事情。
由于Node异步的天性自然,这是不容易办到的,看下代码里面的例子
否则:在没有上下文的情况下查看生产错误日志,这会使问题变得更加困难和缓慢去解决。

Winston logger

  1. // 集中式logger对象
  2. var logger = new winston.Logger({
  3. level: 'info',
  4. transports: [
  5. new (winston.transports.Console)(),
  6. new (winston.transports.File)({ filename: 'somefile.log' })
  7. ]
  8. });
  9. //在某个地方使用logger的自定义代码
  10. logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });

查询日志文件夹 (搜索条目)

  1. var options = {
  2. from: new Date - 24 * 60 * 60 * 1000,
  3. until: new Date,
  4. limit: 10,
  5. start: 0,
  6. order: 'desc',
  7. fields: ['message']
  8. };
  9. // 查找在今天和昨天之间记录的项目
  10. winston.query(options, function (err, results) {
  11. //对于结果的回调处理
  12. });

Pino