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个核心技巧》
相关文章
相关标签/搜索