【NJ09】NodeJS服务的其他实践

175 阅读2分钟

异常处理

处理未捕获的异常

  • 除非开发者记得添加.catch语句,在这些地方抛出的错误都不会被 uncaughtException 事件处理程序来处理,然后消失掉。
  • Node 应用不会奔溃,但可能导致内存泄露
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模块)

  • 通过 domain 模块的 create 方法创建实例
  • 某个错误已经任何其他错误都会被同一个 error 处理方法处理
  • 任何在这个回调中导致错误的代码都会被 domain 覆盖到
  • 允许我们代码在一个沙盒运行,并且可以使用 res 对象给用户反馈
const domain = require('domain');
const audioDomain = domain.create();

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

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

使用 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')

性能实践

避免使用 Lodash

  • 使用像 lodash 这样的方法库这会导致不必要的依赖和较慢的性能
  • 随着新的 V8 引擎和新的 ES 标准的引入,原生方法得到了改进,现在性能比方法库提高了 50%

使用 ESLint 插件检测:

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

使用 prof 进行性能分析

  • 使用 tick-processor 工具处理分析
node --prof profile-test.js
npm install tick -g

node-tick-processor

应用安全清单

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:不显示。

koa-ratelimit 限制并发请求

DOS 攻击非常流行而且相对容易处理。使用外部服务,比如 cloud 负载均衡, cloud 防火墙, nginx, 或者(对于小的,不是那么重要的app)一个速率限制中间件(比如 koa-ratelimit),来实现速率限制。

使用 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 {
   // 密码不匹配
  } 
});

防止 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'));

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

默认情况下, 集成的 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: {}
    });
});

JWT

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。