silenceboy

silenceboy 查看完整档案

杭州编辑河南大学  |  计算机与信息工程学院 编辑湖北亿咖通  |  nodejs后端开发 编辑 www.silenceboy.com 编辑
编辑

coding for fun

个人动态

silenceboy 发布了文章 · 2019-07-16

docker安装gitlab

docker安装gitlab

GitLab 分为 社区版(Community Edition,缩写为 CE)和 企业版(Enterprise Edition,缩写为 EE)。社区版是免费的,而企业版包含一些收费服务,一般来说个人开发者用社区版就足够了。

下载镜像

首先需要先下载 GitLab CE 的镜像,使用下面的命令进行下载,因为文件较大,所以可能需要一点时间,耐心等待即可。
# 不加 tag 则默认为最新版本 latest
$ docker pull gitlab/gitlab-ce

创建目录

通常会将 GitLab 的配置 (etc) 、 日志 (log) 、数据 (data) 放到容器之外, 便于日后升级, 因此请先准备这三个目录。
mkdir -p /srv/gitlab/config
mkdir -p /srv/gitlab/logs 
mkdir -p /srv/gitlab/data

启动运行

$ docker run --detach \
  --hostname gitlab.example.com \
  --publish 8443:443 --publish 8880:80 --publish 8222:22 \
  --name gitlab \
  --restart always \
  --volume /srv/gitlab/config:/etc/gitlab \
  --volume /srv/gitlab/logs:/var/log/gitlab \
  --volume /srv/gitlab/data:/var/opt/gitlab \
  --privileged=true \
  gitlab/gitlab-ce:latest

说明:

  • --hostname gitlab.example.com: 设置主机名或域名
  • --publish 8443:443:将http:443映射到外部端口8443
  • --publish 8880:80:将web:80映射到外部端口8880
  • --publish 8222:22:将ssh:22映射到外部端口8222
  • --name gitlab: 运行容器名
  • --restart always: 自动重启
  • --volume /srv/gitlab/config:/etc/gitlab: 挂载目录
  • --volume /srv/gitlab/logs:/var/log/gitlab: 挂载目录
  • --volume /srv/gitlab/data:/var/opt/gitlab: 挂载目录
  • --privileged=true 使得容器内的root拥有真正的root权限。否则,container内的root只是外部的一个普通用户权限

运行成功之后,可以使用下面的命令查看容器运行状态:

$ docker ps

可以看到 GitLab 已经在运行了,有一个属性 STATUS 为 health: starting,说明 gitlab 的服务正在启动中,还没有启动完毕。等这个状态变成 healthy 时则说明已经部署完成,可以访问了。

访问

gitlab启动成功后,浏览器访问http://ip:8880, 即可访问。为了使用域名访问,需要配置nginx:

upstream gitlab{
    server 127.0.0.1:8880;
}

server {
    listen 80;
    server_name  gitlab.example.com;
    access_log     /var/log/nginx/gitlab.example.com-access.log;
    error_log     /var/log/nginx/gitlab.example.com-error.log;
    location / {
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_pass http://gitlab;
    }
}

nginx重启配置生效后,浏览器访问http://gitlab.example.com 即可正常访问。

首次访问需要为root用户设置密码,设置完成后需要登录,默认用户名为:root, 密码为刚刚设置的密码。

配置邮件服务器

想要让 GitLab 给你发送邮件,还要配置一下邮件服务器,这里以QQ邮箱的 IMAP/SMTP服务 来配置。

打开邮箱->设置->账户,然后开启 IMAP/SMTP服务,然后根据文档获取 授权码 ,这步比较重要。

然后跳转至挂载目录 /srv/gitlab/config/ 编辑gitlab.rb 文件,找到 Email Settings的注释位置,然后修改以下内容:

### Email Settings
gitlab_rails['smtp_enable'] = true # 开启 SMTP 功能
gitlab_rails['smtp_address'] = "smtp.qq.com"
gitlab_rails['smtp_port'] = 465 # 端口不可以选择587,测试过会发送邮件失败
gitlab_rails['smtp_user_name'] = "test@qq.com" # 你的邮箱账号
gitlab_rails['smtp_password'] = "1324dasd" # 授权码,不是密码
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = true
gitlab_rails['gitlab_email_from'] = 'test@qq.com' # 发件人信息,必须跟‘smtp_user_name’保持一致,否则报错
gitlab_rails['smtp_domain'] = "qq.com" # 修改并不影响 

配置完成后保存,然后输入下面的命令使配置生效。

$ sudo docker exec gitlab gitlab-ctl reconfigure

使配置生效之后我们可以使用 gitlab 自带的工具进行一下测试。依次执行下面的命令:

# 开启 gitlab 的 bash 工具
$ docker exec -it gitlab bash

# 开启 gitlab-rails 工具
$ gitlab-rails console production

# 发送邮件进行测试
Notify.test_email('test_001@123.com', 'Message Subject', 'Message Body').deliver_now

测试完成之后退出gitlab的bash工具,重启 gitlab 即可。

$ docker restart gitlab

配置 Git 仓库访问路径

在之前第一次运行 gitlab 容器的时候,有一个参数 hostname 为 gitlab.example.com , 如果配置了域名可以忽略这一步,如果你没有配置相应域名的话,你的仓库的地址将会变为下面这样:

ssh : git@gitlab.example.com:test/test.git
http:gitlab.example.com/test/test.git

如果域名不存在的话,这个地址是无法进行 clone 的。

为了解决这个问题,我们可以设置成 IP 或 你配置了的域名来访问。

打开文件 /srv/gitlab/config/gitlab.rb 文件并找到

# external_url 'GENERATED_EXTERNAL_URL'

这行,去掉注释,并按照下面的格式修改。

# ip 形式
external_url 'http://192.168.1.44'

# 域名形式
external_url 'http://JemGeek.com'

# 子域名
external_url 'http://gitlab.JemGeek.com'

# 其他形式
external_url 'http://JemGeek.com/gitlab'

以上形式都是可以的。修改完成后,输入命令:

$ docker exec gitlab gitlab-ctl reconfigure

使配置生效,然后重启 gitlab 即可。

升级

参照官方的说明, 将原来的容器停止, 然后删除:

$ docker stop gitlab
$ docker rm gitlab

然后重新拉一个新版本的镜像下来,

$ docker pull gitlab/gitlab-ce

使用原来的运行命令运行:

$ docker run --detach \
  --hostname gitlab.example.com \
  --publish 8443:443 --publish 8880:80 --publish 8222:22 \
  --name gitlab \
  --restart always \
  --volume /srv/gitlab/config:/etc/gitlab \
  --volume /srv/gitlab/logs:/var/log/gitlab \
  --volume /srv/gitlab/data:/var/opt/gitlab \
  --privileged=true \
  gitlab/gitlab-ce:latest

GitLab 在初次运行的时候会自动升级, 为了预防万一, 还是建议先备份一下 /srv/gitlab/ 这个目录。

大版本升级(例如从 8.7.x 升级到 8.8.x)用上面的操作有可能会出现错误, 如果出现错误可以尝试登录到容器内部, 依次执行下面的命令:

$ gitlab-ctl reconfigure
$ gitlab-ctl restart
查看原文

赞 13 收藏 8 评论 3

silenceboy 赞了文章 · 2019-05-08

用Node.js创建安全的 GraphQL API

翻译:疯狂的技术宅
https://www.toptal.com/graphq...

本文首发微信公众号:前端先锋
欢迎关注,每天都给你推送新鲜的前端技术文章


本文的目标是提供关于如何创建安全的 Node.js GraphQL API 的快速指南。

你可能会想到一些问题:

  • 使用 GraphQL API 的目的是什么?
  • 什么是GraphQL API?
  • 什么是GraphQL查询?
  • GraphQL的好处是什么?
  • GraphQL是否优于REST?
  • 为什么我们使用Node.js?

这些问题都是有意义的,但在回答之前,我们应该深入了解当前 Web 开发的状态:

  • 现在几乎所有的解决方案都使用了某种应用程序编程接口(API)。
  • 即使你只用社交网络(如Facebook或Instagram),仍然会用到使用API​​的前端。
  • 如果你感到好奇,你会发现几乎所有在线娱乐服务都在用不同类型的API,包括Netflix,Spotify和YouTube等。

你会发现几乎在每种情况下都会有一个不需要你去详细了解的API,例如你不需要知道它们是怎样构建的,并且不需要使用与他们相同的技术就能够将其集成到你自己的系统中。API允许你提供一种可以在服务器和客户端通信之间进行通用标准通信的方式,而不必依赖于特定的技术栈。

通过结构良好的API,可以拥有可靠、可维护且可扩展的API,可以为多种客户端和前端应用提供服务。

什么是 GraphQL API?

GraphQL 是一种 API 所使用的查询语言,由Facebook开发并用于其内部项目,并于2015年公开发布。它支持读取、写入和实时更新等操作。同时它也是开源的,通常会与REST和其他架构放在一起进行比较。简而言之,它基于:

  • GraphQL查询 —— 允许客户端进行读取和控制接收数据的方式。
  • GraphQL 修改 —— 描述怎样在服务器上写入数据。关于怎样将数据写入系统的GraphQL约定。

虽然本文应该展示一个关于如何构建和使用GraphQL API的简单但真实的场景,但我们不会去详细介绍GraphQL。因为GraphQL团队提供了全面的文档,并在Introduction to GraphQL中列出了几个最佳实践。

什么是GraphQL查询?

如上所述,查询是客户端从API读取和操作数据的一种方式。你可以传递对象的类型,并选择要接收的字段类型。下面是一个简单的查询:

query{
  users{
    firstName,
    lastName
  }
}

我们尝试从用户库中查询所有用户,但只接收firstNamelastName。此查询的结果将类似于:

{
  "data": {
    "users": [
      {
        "firstName": "Marcos",
        "lastName": "Silva"
      },
      {
        "firstName": "Paulo",
        "lastName": "Silva"
      }
    ]
  }
}

客户端的使用非常简单。

使用GraphQL API的目的是什么?

创建API的目的是使自己的软件具有可以被其他外部服务集成的能力。即使你的程序被单个前端程序所使用,也可以将此前端视为外部服务,为此,当通过API为两者之间提供通信时,你能够在不同的项目中工作。

如果你在一个大型团队中工作,可以将其拆分为创建前端和后端团队,从而允许他们使用相同的技术,并使他们的工作更轻松。

在本文中,我们将重点介绍怎样构建使用GraphQL API的框架。

GraphQL比REST更好吗?

GraphQL是一种适合多种情况的方法。 REST是一种体系结构方法。如今,有大量的文章可以解释为什么一个比另一个好,或者为什么你应该只使用REST而不是GraphQL。另外你可以通过多种方式在内部使用GraphQL,并将API的端点维护为基于REST的架构。

你应该做的是了解每种方法的好处,分析自己正在创建的解决方案,评估你的团队使用解决方案的舒适程度,并评估你是否能够指导你的团队快速掌握这些技术。

本文更偏重于实用指南,而不是GraphQL和REST的主观比较。如果你想查看这两者的详细比较,我建议你查看我们的另一篇文章,为什么GraphQL是API的未来

在今天的文章中,我们将专注于怎样用Node.js创建GraphQL API。

为什么要使用Node.js?

GraphQL有好几个不同的支持库可供使用。出于本文的目的,我们决定使用Node.js环境下的库,因为它的应用非常广泛,并且Node.js允许开发人员使用他们熟悉的前端语法进行服务器端开发。

掌握GraphQL

我们将为自己的 GraphQL API 设计一个构思的框架,在开始之前,你需要了解Node.js和Express的基础知识。这个GraphQL示例项目的源代码可以在这里找到(https://github.com/makinhs/no...)。

我们将会处理两种类型的资源:

  • Users ,处理基本的CRUD。
  • Products, 我们对它的介绍会详细一点,以展示GraphQL更多的功能。

Users 包含以下字段:

  • id
  • firstname
  • lastname
  • email
  • password
  • permissionLevel

Products 包含以下字段:

  • id
  • name
  • description
  • price

至于编码标准,我们将在这个项目中使用TypeScript。

让我们开始编码!

首先,要确保安装了最新的Node.js版本。在本文发布时,在Nodejs.org上当前版本为10.15.3。

初始化项目

让我们创建一个名为node-graphql的新文件夹,并在终端或Git CLI控制台下使用以下命令:npm init

配置依赖项和TypeScript

为了节约时间,在我们的Git存储库中找到以下代码去替换你的package.json应该包含的依赖项:

{
  "name": "node-graphql",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "tsc": "tsc",
    "start": "npm run tsc && node ./build/app.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/express": "^4.16.1",
    "@types/express-graphql": "^0.6.2",
    "@types/graphql": "^14.0.7",
    "express": "^4.16.4",
    "express-graphql": "^0.7.1",
    "graphql": "^14.1.1",
    "graphql-tools": "^4.0.4"
  },
  "devDependencies": {
    "tslint": "^5.14.0",
    "typescript": "^3.3.4000"
  }
}

更新package.json后,在终端中执行:npm install

接着是配置我们的TypeScript模式。在根文件夹中创建一个名为tsconfig.json的文件,其中包含以下内容:

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true
  }
}

这个配置的代码逻辑将会出现在app文件夹中。在那里我们可以创建一个app.ts文件,在里面添加以下代码用于基本测试:

console.log('Hello Graphql Node API tutorial');

通过前面的配置,现在我们可以运行 npm start 进行构建和测试了。在终端控制台中,你应该能够看到输出的字符串“Hello Graphql Node API tutorial”。在后台场景中,我们的配置会将 TypeScript 代码编译为纯 JavaScript,然后在build文件夹中执行构建。

现在为GraphQL API配置一个基本框架。为了开始我们的项目,将添加三个基本的导入:

  • Express
  • Express-graphql
  • Graphql-tools

把它们放在一起:

import express from 'express';
import graphqlHTTP from 'express-graphql';
import {makeExecutableSchema} from 'graphql-tools';

现在应该能够开始编码了。下一步是在Express中处理我们的程序和基本的GraphQL配置,例如:

import express from 'express';
import graphqlHTTP from 'express-graphql';
import {makeExecutableSchema} from 'graphql-tools';

const app: express.Application = express();
const port = 3000;


let typeDefs: any = [`
  type Query {
    hello: String
  }
     
  type Mutation {
    hello(message: String) : String
  }
`];

let helloMessage: String = 'World!';

let resolvers = {
    Query: {
        hello: () => helloMessage
    },
    Mutation: {
        hello: (_: any, helloData: any) => {
            helloMessage = helloData.message;
            return helloMessage;
        }
    }
};


app.use(
    '/graphql',
    graphqlHTTP({
        schema: makeExecutableSchema({typeDefs, resolvers}),
        graphiql: true
    })
);
app.listen(port, () => console.log(`Node Graphql API listening on port ${port}!`));

我们正在做的是:

  • 为Express服务器启用端口3000。
  • 定义我们想要用作快速示例的查询和修改。
  • 定义查询和修改的工作方式。

好的,但是typeDefs和resolvers中发生了什么,它们与查询和修改的关系又是怎样的呢?

  • typeDefs - 我们可以从查询和修改中获得的模式的定义。
  • Resolvers - 在这里我们定义了查询和修改的功能和行为,而不是想要的字段或参数。
  • Queries - 我们想要从服务器读取的“获取方式”。
  • Mutations - 我们的请求将会影响在自己的服务器上的数据。

现在让我们再次运行npm start,看看我们能得到些什么。我们希望该程序运行后产生这种效果:Graphql API 侦听3000端口。

我们现在可以试着通过访问 http://localhost:3000/graphql 查询和测试GraphQL API:

clipboard.png

好了,现在可以编写第一个自己的查询了,先定义为“hello”。

clipboard.png

请注意,我们在typeDefs中定义它的方式,页面可以帮助我们构建查询。

这很好,但我们怎样才能改变值呢?当然是mutation!

现在,让我们看看当我们用mutation对值进行改变时会发生什么:

clipboard.png

现在我们可以用GraphQL Node.js API进行基本的CRUD操作了。接下来开始使用这些代码。

Products

对于Products,我们将使用名为products的模块。为了是本文不那么啰嗦,我们将用内存数据库进行演示。先定义一个模型和服务来管理Products。

我们的模型将基于以下内容:

export class Product {
  private id: Number = 0;
  private name: String = '';
  private description: String = '';
  private price: Number = 0;

  constructor(productId: Number,
    productName: String,
    productDescription: String,
    price: Number) {
    this.id = productId;
    this.name = productName;
    this.description = productDescription;
    this.price = price;
  }

}

与GraphQL通信的服务定义为:

export class ProductsService {

    public products: any = [];

    configTypeDefs() {
        let typeDefs = `
          type Product {
            name: String,
            description: String,
            id: Int,
            price: Int
          } `;
        typeDefs += ` 
          extend type Query {
          products: [Product]
        }
        `;

        typeDefs += `
          extend type Mutation {
            product(name:String, id:Int, description: String, price: Int): Product!
          }`;
        return typeDefs;
    }

    configResolvers(resolvers: any) {
        resolvers.Query.products = () => {
            return this.products;
        };
        resolvers.Mutation.product = (_: any, product: any) => {
            this.products.push(product);
            return product;
        };

    }

}

Users

对于users,我们将遵循与products模块相同的结构。我们将为用户提供模型和服务。该模型将定义为:

export class User {
    private id: Number = 0;
    private firstName: String = '';
    private lastName: String = '';
    private email: String = '';
    private password: String = '';
    private permissionLevel: Number = 1;

    constructor(id: Number,
                firstName: String,
                lastName: String,
                email: String,
                password: String,
                permissionLevel: Number) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.password = password;
        this.permissionLevel = permissionLevel;
    }

}

同时,我们的服务将会是这样:

const crypto = require('crypto');

export class UsersService {

    public users: any = [];

    configTypeDefs() {
        let typeDefs = `
          type User {
            firstName: String,
            lastName: String,
            id: Int,
            password: String,
            permissionLevel: Int,
            email: String
          } `;
        typeDefs += ` 
          extend type Query {
          users: [User]
        }
        `;

        typeDefs += `
          extend type Mutation {
            user(firstName:String,
             lastName: String,
             password: String,
             permissionLevel: Int,
             email: String,
             id:Int): User!
          }`;
        return typeDefs;
    }

    configResolvers(resolvers: any) {
        resolvers.Query.users = () => {
            return this.users;
        };
        resolvers.Mutation.user = (_: any, user: any) => {
            let salt = crypto.randomBytes(16).toString('base64');
            let hash = crypto.createHmac('sha512', salt).update(user.password).digest("base64");
            user.password = hash;
            this.users.push(user);
            return user;
        };

    }

}

提醒一下,源代码可以在 https://github.com/makinhs/no... 找到。

现在运行并测试我们的代码。运行npm start,将在端口3000上运行服务器。我们现在可以通过访问http://localhost:3000/graphql来测试自己的GraphQL

尝试一个mutation,将一个项目添加到我们的product列表中:

clipboard.png

为了测试它是否有效,我们现在使用查询,但只接收idnameprice

query{
  products{
    id,
    name,
    price
  }
}

将会返回:
{
  "data": {
    "products": [
          {
        "id": 100,
        "name": "My amazing product",
        "price": 400
      }
    ]
  }
}

很好,按照预期工作了。现在可以根据需要获取字段了。你可以试着添加一些描述:

query{
  products{
    id,
    name,
    description,
    price
  }
}

现在我们可以对product进行描述。接下来试试user吧。

mutation{
  user(id:200,
  firstName:"Marcos",
  lastName:"Silva",
  password:"amaz1ingP4ss",
  permissionLevel:9,
  email:"marcos.henrique@toptal.com") {
    id
  }
}

查询如下:

query{
  users{
    id,
    firstName,
    lastName,
    password,
    email
  }
}

返回内容如下:

{
  "data": {
    "users": [
      {
        "id": 200,
        "firstName": "Marcos",
        "lastName": "Silva",
        "password": "kpj6Mq0tGChGbZ+BT9Nw6RMCLReZEPPyBCaUS3X23lZwCCp1Ogb94/oqJlya0xOBdgEbUwqRSuZRjZGhCzLdeQ==",
        "email": "marcos.henrique@toptal.com"
      }
    ]
  }
}

到此为止,我们的GraphQL骨架完成!虽然离实现一个有用的、功能齐全的API还需要很多步骤,但现在已经设置好了基本的核心功能。

总结和最后的想法

让我们回顾一下本文的内容:

  • 在Node.js下可以通过Express和GraphQL库来构建GraphQL API;
  • 基本的GraphQL使用;
  • 查询和修改的基本用法;
  • 为项目创建模块的基本方法;
  • 测试我们的GraphQL API;

为了集中精力关注GraphQL API本身,我们忽略了几个重要的步骤,可简要总结如下:

  • 新项目的验证;
  • 使用通用的错误服务正确处理异常;
  • 验证用户可以在每个请求中使用的字段;
  • 添加JWT拦截器以保护API;
  • 使用更有效的方法处理密码哈希;
  • 添加单元和集成测试;

请记住,我们在Git (https://github.com/makinhs/no...)上有完整的源代码。可以随意使用、fork、提问、pull 并运行它!请注意,本文中提出的所有标准和建议并不是一成不变的。

这只是设计GraphQL API的众多方法之一。此外,请务必更详细地阅读和探索GraphQL文档,以了解它提供的内容以及怎样使你的API更好。


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:

查看原文

赞 25 收藏 15 评论 1

silenceboy 发布了文章 · 2019-05-06

八种方式实现跨域请求

浏览器的同源策略

提到跨域不能不先说一下”同源策略”。

何为同源?只有当协议、端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。

跨域请求方式


解决跨域问题,最简单的莫过于通过nginx反向代理进行实现,但是其需要在运维层面修改,且有可能请求的资源并不再我们控制范围内(第三方),所以该方式不能作为通用的解决方案,下面阐述了经常用到几种跨域方式:

方式一:图片ping或script标签跨域

图片ping常用于跟踪用户点击页面或动态广告曝光次数。
script标签可以得到从其他来源数据,这也是JSONP依赖的根据。
缺点:只能发送Get请求 ,无法访问服务器的响应文本(单向请求)

方式二:JSONP跨域


JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。

缺点:

只能使用Get请求
不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败
JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保

方式三:CORS


Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响应头的一种或几种:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

跨域请求默认不会携带Cookie信息,如果需要携带,请配置下述参数:

"Access-Control-Allow-Credentials": true
// Ajax设置
"withCredentials": true

方式四:window.name+iframe


window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用。然后,HTML文件将传递给请求者的字符串内容赋值给window.name。然后,请求者可以检索window.name值作为响应。

  • iframe标签的跨域能力;
  • window.name属性值在文档刷新后依旧存在的能力(且最大允许2M左右)。

每个iframe都有包裹它的window,而这个window是top window的子窗口。contentWindow属性返回<iframe>元素的Window对象。你可以使用这个Window对象来访问iframe的文档及其内部DOM。

<!-- 
 下述用端口 
 10000表示:domainA
 10001表示:domainB
-->

<!-- localhost:10000 -->
<script>
  var iframe = document.createElement('iframe');
  iframe.style.display = 'none'; // 隐藏

  var state = 0; // 防止页面无限刷新
  iframe.onload = function() {
      if(state === 1) {
          console.log(JSON.parse(iframe.contentWindow.name));
          // 清除创建的iframe
          iframe.contentWindow.document.write('');
          iframe.contentWindow.close();
          document.body.removeChild(iframe);
      } else if(state === 0) {
          state = 1;
          // 加载完成,指向当前域,防止错误(proxy.html为空白页面)
          // Blocked a frame with origin "http://localhost:10000" from accessing a cross-origin frame.
          iframe.contentWindow.location = 'http://localhost:10000/proxy.html';
      }
  };

  iframe.src = 'http://localhost:10001';
  document.body.appendChild(iframe);
</script>

<!-- localhost:10001 -->
<!DOCTYPE html>
...
<script>
  window.name = JSON.stringify({a: 1, b: 2});
</script>
</html>

注意:

  1. 直接嵌入其他域(localhots:10001)下的URL会报错,所以需要加载完成替换为当前域的URL(localhots:10000),proxy.html为空白页面,只为解决该问题;
  2. 重新设置src(http://localhost:10000/proxy.html)后导致页面不断刷新,所以通过state来控制;
  3. 全部获取完结果后,清除该iframe。

方式五:window.postMessage()


HTML5新特性,可以用来向其他所有的 window 对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送 MessageEvent,如果在函数执行的过程中调用了它,就会让后面的函数超时无法执行。

下述代码实现了跨域存储localStorage

<!-- 
 下述用端口 
 10000表示:domainA
 10001表示:domainB
-->

<!-- localhost:10000 -->
<iframe data-original="http://localhost:10001/msg.html" name="myPostMessage" style="display:none;">
</iframe>

<script>
  function main() {
      LSsetItem('test', 'Test: ' + new Date());
      LSgetItem('test', function(value) {
          console.log('value: ' + value);
      });
      LSremoveItem('test');
  }

  var callbacks = {};
  window.addEventListener('message', function(event) {
      if (event.source === frames['myPostMessage']) {
          console.log(event)
          var data = /^#localStorage#(\d+)(null)?#([\S\s]*)/.exec(event.data);
          if (data) {
              if (callbacks[data[1]]) {
                  callbacks[data[1]](data[2] === 'null' ? null : data[3]);
              }
              delete callbacks[data[1]];
          }
      }
  }, false);

  var domain = '*';
  // 增加
  function LSsetItem(key, value) {
      var obj = {
          setItem: key,
          value: value
      };
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
  // 获取
  function LSgetItem(key, callback) {
      var identifier = new Date().getTime();
      var obj = {
          identifier: identifier,
          getItem: key
      };
      callbacks[identifier] = callback;
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
  // 删除
  function LSremoveItem(key) {
      var obj = {
          removeItem: key
      };
      frames['myPostMessage'].postMessage(JSON.stringify(obj), domain);
  }
</script>

<!-- localhost:10001 -->
<script>
  window.addEventListener('message', function(event) {
    console.log('Receiver debugging', event);
    if (event.origin == 'http://localhost:10000') {
      var data = JSON.parse(event.data);
      if ('setItem' in data) {
        localStorage.setItem(data.setItem, data.value);
      } else if ('getItem' in data) {
        var gotItem = localStorage.getItem(data.getItem);
        event.source.postMessage(
          '#localStorage#' + data.identifier +
          (gotItem === null ? 'null#' : '#' + gotItem),
          event.origin
        );
      } else if ('removeItem' in data) {
        localStorage.removeItem(data.removeItem);
      }
    }
  }, false);
</script>

注意Safari下会报错:

Blocked a frame with origin “http://localhost:10001” from accessing a frame with origin “http://localhost:10000“. Protocols, domains, and ports must match.

避免该错误,可以在Safari浏览器中勾选开发菜单==>停用跨域限制。或者只能使用服务器端转存的方式实现,因为Safari浏览器默认只支持CORS跨域请求。

方式六:修改document.domain跨子域


前提条件:这两个域名必须属于同一个基础域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域,所以只能跨子域

​在根域范围内,允许把domain属性的值设置为它的上一级域。例如,在”aaa.xxx.com”域内,可以把domain设置为 “xxx.com” 但不能设置为 “xxx.org” 或者”com”。

现在存在两个域名aaa.xxx.com和bbb.xxx.com。在aaa下嵌入bbb的页面,由于其document.name不一致,无法在aaa下操作bbb的js。可以在aaa和bbb下通过js将document.name = 'xxx.com';设置一致,来达到互相访问的作用。

方式七:WebSocket

WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很棒的实现。相关文章,请查看:WebSocket、WebSocket-SockJS

需要注意:WebSocket对象不支持DOM 2级事件侦听器,必须使用DOM 0级语法分别定义各个事件。

方式八:代理

同源策略是针对浏览器端进行的限制,可以通过服务器端来解决该问题

DomainA客户端(浏览器) ==> DomainA服务器 ==> DomainB服务器 ==> DomainA客户端(浏览器)

查看原文

赞 0 收藏 0 评论 0

silenceboy 收藏了文章 · 2019-05-05

NodeJS实现简易区块链

之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。

但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。

想了想,就动手做试试咯。

专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xin-tan.com | GitHub

技术调研

在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。

结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。

区块定义

为了方便表示区块,将其封装为一个class,它没有任何方法:

/**
 * 区块信息的结构化定义
 */
class Block {
  /**
   * 构造函数
   * @param {Number} index 
   * @param {String} previousHash 
   * @param {Number} timestamp 
   * @param {*} data 
   * @param {String} hash 
   */
  constructor(index, previousHash, timestamp, data, hash) {
    this.index = index // 区块的位置
    this.previousHash = previousHash + '' // 前一个区块的hash
    this.timestamp = timestamp // 生成区块时候的时间戳
    this.data = data // 区块本身携带的数据
    this.hash = hash + '' // 区块根据自身信息和规则生成的hash
  }
}

至于怎么生成hash,这里采用的规则比较简单:

  1. 拼接index、previouHash、timestamp和data,将其字符串化
  2. 利用sha256算法,计算出的记过就是hash

为了方便,会引入一个加密库:

const CryptoJS = require('crypto-js')

链结构定义

很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:

  1. 按照加密规则生成hash
  2. 插入新块和检查操作
  3. 批量插入块和检查操作以及可信度计算

1. 起源块

起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。

class BlockChain {
  constructor() {
    this.blocks = [this.getGenesisBlock()]
  }

  /**
   * 创建区块链起源块, 此块是硬编码
   */
  getGenesisBlock() {
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')
  }
}

2. 计算下一个区块

BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。

// 方法都是BlockChain对象方法
  /**
   * 根据信息计算hash值
   */
  calcuteHash(index, previousHash, timestamp, data) {
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''
  }

  /**
   * 得到区块链中最后一个块节点
   */
  getLatestBlock() {
    return this.blocks[this.blocks.length - 1]
  }

  /**
   * 计算当前链表的下一个区块
   * @param {*} blockData 
   */
  generateNextBlock(blockData) {
    const previousBlock = this.getLatestBlock()
    const nextIndex = previousBlock.index + 1
    const nextTimeStamp = new Date().getTime()
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
  }

3. 插入区块

插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。

  /**
   * 向区块链添加新节点
   * @param {Block} newBlock 
   */
  addBlock(newBlock) {
    // 合法区块
    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {
      this.blocks.push(newBlock)
      return true  
    }
    return false
  }

检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:

  1. 判断新区块的index是否是递增的
  2. 判断previousHash是否和前一个区块的hash相等
  3. 判断新区块的hash是否按约束好的规则生成
  /**
   * 判断新加入的块是否合法
   * @param {Block} newBlock 
   * @param {Block} previousBlock 
   */
  isValidNewBlock(newBlock, previousBlock) {
    if(
      !(newBlock instanceof Block) ||
      !(previousBlock instanceof Block)
    ) {
      return false
    }

    // 判断index
    if(newBlock.index !== previousBlock.index + 1) { 
      return false
    }

    // 判断hash值
    if(newBlock.previousHash !== previousBlock.hash) { 
      return false
    }

    // 计算新块的hash值是否符合规则
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) { 
      return false
    }

    return true
  }

4. 批量插入

批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。

在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。

但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length(C) > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链

插入新链的方法如下:

  /**
   * 插入新链表
   * @param {Array} newChain 
   */
  addChain(newChain) {
    if(this.isValidNewChain(newChain)) {
      const index = newChain[0].index
      this.blocks.splice(index)
      this.blocks = this.blocks.concat(newChain)
      return true
    }
    return false
  }

实现上面所述逻辑的方法如下:

  /**
   * 判断新插入的区块链是否合法而且可以覆盖原来的节点
   * @param {Array} newChain 
   */
  isValidNewChain(newChain) {
    if(Array.isArray(newChain) === false || newChain.length === 0) {
      return false
    }

    let newChainLength = newChain.length,
      firstBlock = newChain[0]

    // 硬编码的起源块不能改变
    if(firstBlock.index === 0) {
      return false
    }

    // 移植新的链的长度 <= 现有链的长度
    // 新的链不可信
    if(newChainLength + firstBlock.index <= this.blocks.length) {
      return false
    }

    // 下面检查新的链能否移植
    // 以及新的链的每个节点是否符合规则
    if(!this.isValidNewBlock(firstBlock, this.blocks[firstBlock.index - 1])) {
      return false
    }

    for(let i = 1; i < newChainLength; ++i) {
      if(!this.isValidNewBlock(newChain[i], newChain[i - 1])) {
        return false
      }
    }

    return true
  }

5. 为什么需要批量插入?

我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。

A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。

而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。

效果测试

虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:

image.png

红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。

全部代码在都放在: https://github.com/dongyuanxin/node-blockchain

更多系列文章

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

silenceboy 赞了文章 · 2019-05-05

NodeJS实现简易区块链

之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。

但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。

想了想,就动手做试试咯。

专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xin-tan.com | GitHub

技术调研

在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。

结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。

区块定义

为了方便表示区块,将其封装为一个class,它没有任何方法:

/**
 * 区块信息的结构化定义
 */
class Block {
  /**
   * 构造函数
   * @param {Number} index 
   * @param {String} previousHash 
   * @param {Number} timestamp 
   * @param {*} data 
   * @param {String} hash 
   */
  constructor(index, previousHash, timestamp, data, hash) {
    this.index = index // 区块的位置
    this.previousHash = previousHash + '' // 前一个区块的hash
    this.timestamp = timestamp // 生成区块时候的时间戳
    this.data = data // 区块本身携带的数据
    this.hash = hash + '' // 区块根据自身信息和规则生成的hash
  }
}

至于怎么生成hash,这里采用的规则比较简单:

  1. 拼接index、previouHash、timestamp和data,将其字符串化
  2. 利用sha256算法,计算出的记过就是hash

为了方便,会引入一个加密库:

const CryptoJS = require('crypto-js')

链结构定义

很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:

  1. 按照加密规则生成hash
  2. 插入新块和检查操作
  3. 批量插入块和检查操作以及可信度计算

1. 起源块

起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。

class BlockChain {
  constructor() {
    this.blocks = [this.getGenesisBlock()]
  }

  /**
   * 创建区块链起源块, 此块是硬编码
   */
  getGenesisBlock() {
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')
  }
}

2. 计算下一个区块

BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。

// 方法都是BlockChain对象方法
  /**
   * 根据信息计算hash值
   */
  calcuteHash(index, previousHash, timestamp, data) {
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''
  }

  /**
   * 得到区块链中最后一个块节点
   */
  getLatestBlock() {
    return this.blocks[this.blocks.length - 1]
  }

  /**
   * 计算当前链表的下一个区块
   * @param {*} blockData 
   */
  generateNextBlock(blockData) {
    const previousBlock = this.getLatestBlock()
    const nextIndex = previousBlock.index + 1
    const nextTimeStamp = new Date().getTime()
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
  }

3. 插入区块

插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。

  /**
   * 向区块链添加新节点
   * @param {Block} newBlock 
   */
  addBlock(newBlock) {
    // 合法区块
    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {
      this.blocks.push(newBlock)
      return true  
    }
    return false
  }

检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:

  1. 判断新区块的index是否是递增的
  2. 判断previousHash是否和前一个区块的hash相等
  3. 判断新区块的hash是否按约束好的规则生成
  /**
   * 判断新加入的块是否合法
   * @param {Block} newBlock 
   * @param {Block} previousBlock 
   */
  isValidNewBlock(newBlock, previousBlock) {
    if(
      !(newBlock instanceof Block) ||
      !(previousBlock instanceof Block)
    ) {
      return false
    }

    // 判断index
    if(newBlock.index !== previousBlock.index + 1) { 
      return false
    }

    // 判断hash值
    if(newBlock.previousHash !== previousBlock.hash) { 
      return false
    }

    // 计算新块的hash值是否符合规则
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) { 
      return false
    }

    return true
  }

4. 批量插入

批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。

在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。

但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length(C) > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链

插入新链的方法如下:

  /**
   * 插入新链表
   * @param {Array} newChain 
   */
  addChain(newChain) {
    if(this.isValidNewChain(newChain)) {
      const index = newChain[0].index
      this.blocks.splice(index)
      this.blocks = this.blocks.concat(newChain)
      return true
    }
    return false
  }

实现上面所述逻辑的方法如下:

  /**
   * 判断新插入的区块链是否合法而且可以覆盖原来的节点
   * @param {Array} newChain 
   */
  isValidNewChain(newChain) {
    if(Array.isArray(newChain) === false || newChain.length === 0) {
      return false
    }

    let newChainLength = newChain.length,
      firstBlock = newChain[0]

    // 硬编码的起源块不能改变
    if(firstBlock.index === 0) {
      return false
    }

    // 移植新的链的长度 <= 现有链的长度
    // 新的链不可信
    if(newChainLength + firstBlock.index <= this.blocks.length) {
      return false
    }

    // 下面检查新的链能否移植
    // 以及新的链的每个节点是否符合规则
    if(!this.isValidNewBlock(firstBlock, this.blocks[firstBlock.index - 1])) {
      return false
    }

    for(let i = 1; i < newChainLength; ++i) {
      if(!this.isValidNewBlock(newChain[i], newChain[i - 1])) {
        return false
      }
    }

    return true
  }

5. 为什么需要批量插入?

我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。

A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。

而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。

效果测试

虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:

image.png

红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。

全部代码在都放在: https://github.com/dongyuanxin/node-blockchain

更多系列文章

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

赞 26 收藏 19 评论 1

silenceboy 赞了文章 · 2019-04-09

[译]Node.js安全清单

前言

安全性,总是一个不可忽视的问题。许多人都承认这点,但是却很少有人真的认真地对待它。所以我们列出了这个清单,让你在将你的应用部署到生产环境来给千万用户使用之前,做一个安全检查。

以下列出的安全项,大多都具有普适性,适用于除了Node.js外的各种语言和框架。但是,其中也包含一些用Node.js写的小工具。

配置管理

安全性相关的HTTP头

以下是一些安全性相关的HTTP头,你的站点应该设置它们:

  • Strict-Transport-Security:强制使用安全连接(SSL/TLS之上的HTTPS)来连接到服务器。

  • X-Frame-Options:提供对于“点击劫持”的保护。

  • X-XSS-Protection:开启大多现代浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。

  • X-Content-Type-Options: 防止浏览器使用MIME-sniffing来确定响应的类型,转而使用明确的content-type来确定。

  • Content-Security-Policy:防止受到跨站脚本攻击以及其他跨站注入攻击。

Node.js中,这些都可以通过使用Helmet模块轻松设置完毕:

var express = require('express');  
var helmet = require('helmet');

var app = express();

app.use(helmet());  

Helmet在Koa中也能使用:koa-helmet

当然,在许多的架构中,这些头会在Web服务器(Apache,nginx)的配置中设置,而不是在应用的代码中。如果是通过nginx配置,配置文件会类似于如下例子:

# nginx.conf

add_header X-Frame-Options SAMEORIGIN;  
add_header X-Content-Type-Options nosniff;  
add_header X-XSS-Protection "1; mode=block";  
add_header Content-Security-Policy "default-src 'self'";  

完整的例子可以参考这个nginx配置

如果你想快速确认你的网站是否都设置这些HTTP头,你可以通过这个网站在线检查:http://cyh.herokuapp.com/cyh

客户端的敏感数据

当部署前端应用时,确保不要在代码中暴露如密钥这样的敏感数据,这将可以被所有人看到。

现今并没有什么自动化检测它们的办法,但是还是有一些手段可以用来减少不小心将敏感数据暴露在客户端的概率:

  • 使用pull request更新代码

  • 建立起code review机制

身份认证

对于暴力破解的保护

暴力破解即系统地列举所有可能的结果,并逐一尝试,来找到正确答案。在web应用中,用户登陆就特别适合它发挥。

你可以通过限制用户的连接频率来防止这类的攻击。在Node.js中,你可以使用ratelimiter包。

var email = req.body.email;  
var limit = new Limiter({ id: email, db: db });

limit.get(function(err, limit) {

});

当然,你可以将它封装成一个中间件以供你的应用使用。ExpressKoa都已经有现成的不错的中间件:

var ratelimit = require('koa-ratelimit');  
var redis = require('redis');  
var koa = require('koa');  
var app = koa();

var emailBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.body.email;
  }
});

var ipBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.ip;
  }
});

app.post('/login', ipBasedRatelimit, emailBasedRatelimit, handleLogin);  

这里我们所做的,就是限制了在一段给定时间内,用户可以尝试登陆的次数 -- 这减少用户密码被暴力破解的风险。以上例子中的选项都是可以根据你的实际情景所改变的,所以不要简单的复制粘贴它们。。

如果你想要测试你的服务在这些场景下的表现,你可以使用hydra

Session管理

对于cookie的安全使用,其重要性是不言而喻的。特别是对于动态的web应用,在如HTTP这样的无状态协议的之上,它们需要使用cookie来维持状态。

Cookie标示

以下是一个每个cookie可以设置的属性的列表,以及它们的含义:

  • secure - 这个属性告诉浏览器,仅在请求是通过HTTPS传输时,才传递cookie。

  • HttpOnly - 设置这个属性将禁止javascript脚本获取到这个cookie,这可以用来帮助防止跨站脚本攻击。

Cookie域

  • domain - 这个属性用来比较请求URL中服务端的域名。如果域名匹配成功,或这是其子域名,则继续检查path属性。

  • path - 除了域名,cookie可用的URL路径也可以被指定。当域名和路径都匹配时,cookie才会随请求发送。

  • expires - 这个属性用来设置持久化的cookie,当设置了它之后,cookie在指定的时间到达之前都不会过期。

Node.js中,你可以使用cookies包来轻松创建cookie。但是,它是较底层的。在创建应用时,你可能更像使用它的一些封装,如cookie-session

var cookieSession = require('cookie-session');  
var express = require('express');

var app = express();

app.use(cookieSession({  
  name: 'session',
  keys: [
    process.env.COOKIE_KEY1,
    process.env.COOKIE_KEY2
  ]
}));

app.use(function (req, res, next) {  
  var n = req.session.views || 0;
  req.session.views = n++;
  res.end(n + ' views');
});

app.listen(3000);  

(以上例子取自cookie-session模块的文档)

CSRF

跨站请求伪造(CSRF)是一种迫使用户在他们已登录的web应用中,执行一个并非他们原意的操作的攻击手段。这种攻击常常用于那些会改变用户的状态的请求,通常它们并不窃取数据,因为攻击者并不能看到响应的内容。

Node.js中,你可以使用csrf模块来缓和这种攻击。它同样是非常底层的,你可能更喜欢使用如csurf这样的Express中间件。

在路由层,可以会有如下代码:

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

// setup route middlewares 
var csrfProtection = csrf({ cookie: true });  
var parseForm = bodyParser.urlencoded({ extended: false });

// create express app 
var app = express();

// we need this because "cookie" is true in csrfProtection 
app.use(cookieParser());

app.get('/form', csrfProtection, function(req, res) {  
  // pass the csrfToken to the view 
  res.render('send', { csrfToken: req.csrfToken() });
});

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

在展示层,你需要使用CSRF token

<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>  

(以上例子取自csurf模块的文档)

数据合法性

XSS

以下是两种类似的,但是略有不同的攻击方式,一种关于跨站脚本,而另一种则关于存储。

  • 非持久化的XSS攻击 在攻击者向指定的URL的响应HTML中注入可执行的JavaScript代码时发生。

  • 持久化的XSS攻击 在应用存储未经过滤的用户输入时发生。用户输入的代码会在你的应用环境下执行。

为了防御这类攻击,请确保你总是检查并过滤了用户的输入内容。

SQL注入

在用户的输入中包含部分或完整的SQL查询语句时,SQL注入就有可能发生。它可能会读取敏感数据,或是直接删除数据。

例如:

select title, author from books where id=$id  

以上这个例子中,$id来自于用户输入。用户输入2 or 1=1也可以。这个查询可能会变成:

select title, author from books where id=2 or 1=1  

最简单的预防方法则是使用参数化查询(parameterized queries)或预处理语句(prepared statements)。

如果你正在通过Node.js使用PostgreSQL。那么你可以使用node-postgres模块,来创建参数化查询:

var q = 'SELECT name FROM books WHERE id = $1';  
client.query(q, ['3'], function(err, result) {});  

命令注入

攻击者使用命令注入来在远程web服务器中运行系统命令。通过命令注入,攻击者甚至可以取得系统的密码。

实践中,如果你有一个URL:

https://example.com/downloads?file=user1.txt  

它可以变成:

https://example.com/downloads?file=%3Bcat%20/etc/passwd  

在这个例子中,%3B会变成一个分号。所以将会运行多条系统命令。

为了预防这类攻击,请确保总是检查过滤了用户的输入内容。

我们也可以以Node.js的角度来说:

child_process.exec('ls', function (err, data) {  
    console.log(data);
});

child_process.exec的底层,它调用了/bin/sh,所以它是一个bash解释器,而不仅仅是只能执行用户程序。

当用户的输入是一个反引号或$()时,将它们传入这个方法就很危险了。

可以通过使用child_process.execFile来解决上面这个问题。

安全传输

SSL版本,算法,键长度

由于HTTP是明文传输的,所以我们需要通过一个SSL/TLS通道来加密,即HTTPS。如今高级别的加密方式已被普遍使用,但是,如果在服务端缺乏配置,也可能会导致服务端使用低级别的加密,或不加密。

你需要测试:

  • 密码,密钥和重协商(renegotiation)都已经合法妥善得配置完毕。

  • 证书的合法性。

使用如nmapsslyze这样的工具可以使这项工作非常简单。

检查证书信息

nmap --script ssl-cert,ssl-enum-ciphers -p 443,465,993,995 www.example.com

使用sslyze来检查SSL/TSL:

./sslyze.py --regular example.com:443

HSTS

在上文的配置管理章节我们已经对其有了接触 - Strict-Transport-Security头会强制使用HTTPS来连接服务器。以下是一个Twitter的例子:

strict-transport-security:max-age=631138519  

这里的max-age定义了浏览器需要自动将所有HTTP请求转换成HTTPS的秒数。

对于它的测试是非常简单的:

curl -s -D- https://twitter.com/ | grep -i Strict  

拒绝服务

账号锁定

账号锁定用于缓和暴力破解带来的拒绝服务方面的影响。实践中,它意味着,当用户尝试了几次登陆并失败后,将在其后的一段内,禁止他的登陆操作。

可以使用之前提到的rate-limiter来阻止这类攻击。

正则表达式

这类攻击主要是由于一些正则表达式,在极端情况下,会变得性能及其糟糕。这些正则被称为恶魔正则(Evil Regexes):

  • 对于重复文本进行分组

  • 在重复的分组内又有重复内容

    ([a-zA-Z]+)*(a+)+(a|a?)+在如aaaaaaaaaaaaaaaaaaaaaaaa! 这样的输入面前,都是脆弱的。这会引起大量的计算。更多详情可以参考ReDos

可以使用Node.js工具safe-regex这检测你的正则:

$ node safe.js '(beep|boop)*'
true  
$ node safe.js '(a+){10}'
false  

错误处理

错误码,堆栈信息

一些错误场景可能会导致应用泄露底层的应用架构信息,如:like: X-Powered-By:Express

堆栈信息可能自己本身并没有什么用,但它经常能泄露一些攻击者非常感兴趣的信息。将堆栈信息返回出来是非常不好的实践。你需要将它们记录在日志中,而不是展示给用户。

NPM

更强的能力意味着更大的责任 - NPM有这许多可以现成使用的包,但是代价是:你需要检查这些包本身是否存在安全问题。

幸运的是Node Security project(nsp)是一个非常棒的工具,来检查你使用的模块是否是易被一些已知的手段攻击的。

npm i nsp -g  
# either audit the shrinkwrap
nsp audit-shrinkwrap  
# or the package.json
nsp audit-package  

最后

这个清单主要根据OWASP维护的Web Application Security Testing Cheat Sheet所列。

原文链接

https://blog.risingstack.com/node-js-security-checklist/

查看原文

赞 36 收藏 159 评论 4

silenceboy 收藏了文章 · 2019-04-09

[译]Node.js安全清单

前言

安全性,总是一个不可忽视的问题。许多人都承认这点,但是却很少有人真的认真地对待它。所以我们列出了这个清单,让你在将你的应用部署到生产环境来给千万用户使用之前,做一个安全检查。

以下列出的安全项,大多都具有普适性,适用于除了Node.js外的各种语言和框架。但是,其中也包含一些用Node.js写的小工具。

配置管理

安全性相关的HTTP头

以下是一些安全性相关的HTTP头,你的站点应该设置它们:

  • Strict-Transport-Security:强制使用安全连接(SSL/TLS之上的HTTPS)来连接到服务器。

  • X-Frame-Options:提供对于“点击劫持”的保护。

  • X-XSS-Protection:开启大多现代浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。

  • X-Content-Type-Options: 防止浏览器使用MIME-sniffing来确定响应的类型,转而使用明确的content-type来确定。

  • Content-Security-Policy:防止受到跨站脚本攻击以及其他跨站注入攻击。

Node.js中,这些都可以通过使用Helmet模块轻松设置完毕:

var express = require('express');  
var helmet = require('helmet');

var app = express();

app.use(helmet());  

Helmet在Koa中也能使用:koa-helmet

当然,在许多的架构中,这些头会在Web服务器(Apache,nginx)的配置中设置,而不是在应用的代码中。如果是通过nginx配置,配置文件会类似于如下例子:

# nginx.conf

add_header X-Frame-Options SAMEORIGIN;  
add_header X-Content-Type-Options nosniff;  
add_header X-XSS-Protection "1; mode=block";  
add_header Content-Security-Policy "default-src 'self'";  

完整的例子可以参考这个nginx配置

如果你想快速确认你的网站是否都设置这些HTTP头,你可以通过这个网站在线检查:http://cyh.herokuapp.com/cyh

客户端的敏感数据

当部署前端应用时,确保不要在代码中暴露如密钥这样的敏感数据,这将可以被所有人看到。

现今并没有什么自动化检测它们的办法,但是还是有一些手段可以用来减少不小心将敏感数据暴露在客户端的概率:

  • 使用pull request更新代码

  • 建立起code review机制

身份认证

对于暴力破解的保护

暴力破解即系统地列举所有可能的结果,并逐一尝试,来找到正确答案。在web应用中,用户登陆就特别适合它发挥。

你可以通过限制用户的连接频率来防止这类的攻击。在Node.js中,你可以使用ratelimiter包。

var email = req.body.email;  
var limit = new Limiter({ id: email, db: db });

limit.get(function(err, limit) {

});

当然,你可以将它封装成一个中间件以供你的应用使用。ExpressKoa都已经有现成的不错的中间件:

var ratelimit = require('koa-ratelimit');  
var redis = require('redis');  
var koa = require('koa');  
var app = koa();

var emailBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.body.email;
  }
});

var ipBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.ip;
  }
});

app.post('/login', ipBasedRatelimit, emailBasedRatelimit, handleLogin);  

这里我们所做的,就是限制了在一段给定时间内,用户可以尝试登陆的次数 -- 这减少用户密码被暴力破解的风险。以上例子中的选项都是可以根据你的实际情景所改变的,所以不要简单的复制粘贴它们。。

如果你想要测试你的服务在这些场景下的表现,你可以使用hydra

Session管理

对于cookie的安全使用,其重要性是不言而喻的。特别是对于动态的web应用,在如HTTP这样的无状态协议的之上,它们需要使用cookie来维持状态。

Cookie标示

以下是一个每个cookie可以设置的属性的列表,以及它们的含义:

  • secure - 这个属性告诉浏览器,仅在请求是通过HTTPS传输时,才传递cookie。

  • HttpOnly - 设置这个属性将禁止javascript脚本获取到这个cookie,这可以用来帮助防止跨站脚本攻击。

Cookie域

  • domain - 这个属性用来比较请求URL中服务端的域名。如果域名匹配成功,或这是其子域名,则继续检查path属性。

  • path - 除了域名,cookie可用的URL路径也可以被指定。当域名和路径都匹配时,cookie才会随请求发送。

  • expires - 这个属性用来设置持久化的cookie,当设置了它之后,cookie在指定的时间到达之前都不会过期。

Node.js中,你可以使用cookies包来轻松创建cookie。但是,它是较底层的。在创建应用时,你可能更像使用它的一些封装,如cookie-session

var cookieSession = require('cookie-session');  
var express = require('express');

var app = express();

app.use(cookieSession({  
  name: 'session',
  keys: [
    process.env.COOKIE_KEY1,
    process.env.COOKIE_KEY2
  ]
}));

app.use(function (req, res, next) {  
  var n = req.session.views || 0;
  req.session.views = n++;
  res.end(n + ' views');
});

app.listen(3000);  

(以上例子取自cookie-session模块的文档)

CSRF

跨站请求伪造(CSRF)是一种迫使用户在他们已登录的web应用中,执行一个并非他们原意的操作的攻击手段。这种攻击常常用于那些会改变用户的状态的请求,通常它们并不窃取数据,因为攻击者并不能看到响应的内容。

Node.js中,你可以使用csrf模块来缓和这种攻击。它同样是非常底层的,你可能更喜欢使用如csurf这样的Express中间件。

在路由层,可以会有如下代码:

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

// setup route middlewares 
var csrfProtection = csrf({ cookie: true });  
var parseForm = bodyParser.urlencoded({ extended: false });

// create express app 
var app = express();

// we need this because "cookie" is true in csrfProtection 
app.use(cookieParser());

app.get('/form', csrfProtection, function(req, res) {  
  // pass the csrfToken to the view 
  res.render('send', { csrfToken: req.csrfToken() });
});

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

在展示层,你需要使用CSRF token

<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>  

(以上例子取自csurf模块的文档)

数据合法性

XSS

以下是两种类似的,但是略有不同的攻击方式,一种关于跨站脚本,而另一种则关于存储。

  • 非持久化的XSS攻击 在攻击者向指定的URL的响应HTML中注入可执行的JavaScript代码时发生。

  • 持久化的XSS攻击 在应用存储未经过滤的用户输入时发生。用户输入的代码会在你的应用环境下执行。

为了防御这类攻击,请确保你总是检查并过滤了用户的输入内容。

SQL注入

在用户的输入中包含部分或完整的SQL查询语句时,SQL注入就有可能发生。它可能会读取敏感数据,或是直接删除数据。

例如:

select title, author from books where id=$id  

以上这个例子中,$id来自于用户输入。用户输入2 or 1=1也可以。这个查询可能会变成:

select title, author from books where id=2 or 1=1  

最简单的预防方法则是使用参数化查询(parameterized queries)或预处理语句(prepared statements)。

如果你正在通过Node.js使用PostgreSQL。那么你可以使用node-postgres模块,来创建参数化查询:

var q = 'SELECT name FROM books WHERE id = $1';  
client.query(q, ['3'], function(err, result) {});  

命令注入

攻击者使用命令注入来在远程web服务器中运行系统命令。通过命令注入,攻击者甚至可以取得系统的密码。

实践中,如果你有一个URL:

https://example.com/downloads?file=user1.txt  

它可以变成:

https://example.com/downloads?file=%3Bcat%20/etc/passwd  

在这个例子中,%3B会变成一个分号。所以将会运行多条系统命令。

为了预防这类攻击,请确保总是检查过滤了用户的输入内容。

我们也可以以Node.js的角度来说:

child_process.exec('ls', function (err, data) {  
    console.log(data);
});

child_process.exec的底层,它调用了/bin/sh,所以它是一个bash解释器,而不仅仅是只能执行用户程序。

当用户的输入是一个反引号或$()时,将它们传入这个方法就很危险了。

可以通过使用child_process.execFile来解决上面这个问题。

安全传输

SSL版本,算法,键长度

由于HTTP是明文传输的,所以我们需要通过一个SSL/TLS通道来加密,即HTTPS。如今高级别的加密方式已被普遍使用,但是,如果在服务端缺乏配置,也可能会导致服务端使用低级别的加密,或不加密。

你需要测试:

  • 密码,密钥和重协商(renegotiation)都已经合法妥善得配置完毕。

  • 证书的合法性。

使用如nmapsslyze这样的工具可以使这项工作非常简单。

检查证书信息

nmap --script ssl-cert,ssl-enum-ciphers -p 443,465,993,995 www.example.com

使用sslyze来检查SSL/TSL:

./sslyze.py --regular example.com:443

HSTS

在上文的配置管理章节我们已经对其有了接触 - Strict-Transport-Security头会强制使用HTTPS来连接服务器。以下是一个Twitter的例子:

strict-transport-security:max-age=631138519  

这里的max-age定义了浏览器需要自动将所有HTTP请求转换成HTTPS的秒数。

对于它的测试是非常简单的:

curl -s -D- https://twitter.com/ | grep -i Strict  

拒绝服务

账号锁定

账号锁定用于缓和暴力破解带来的拒绝服务方面的影响。实践中,它意味着,当用户尝试了几次登陆并失败后,将在其后的一段内,禁止他的登陆操作。

可以使用之前提到的rate-limiter来阻止这类攻击。

正则表达式

这类攻击主要是由于一些正则表达式,在极端情况下,会变得性能及其糟糕。这些正则被称为恶魔正则(Evil Regexes):

  • 对于重复文本进行分组

  • 在重复的分组内又有重复内容

    ([a-zA-Z]+)*(a+)+(a|a?)+在如aaaaaaaaaaaaaaaaaaaaaaaa! 这样的输入面前,都是脆弱的。这会引起大量的计算。更多详情可以参考ReDos

可以使用Node.js工具safe-regex这检测你的正则:

$ node safe.js '(beep|boop)*'
true  
$ node safe.js '(a+){10}'
false  

错误处理

错误码,堆栈信息

一些错误场景可能会导致应用泄露底层的应用架构信息,如:like: X-Powered-By:Express

堆栈信息可能自己本身并没有什么用,但它经常能泄露一些攻击者非常感兴趣的信息。将堆栈信息返回出来是非常不好的实践。你需要将它们记录在日志中,而不是展示给用户。

NPM

更强的能力意味着更大的责任 - NPM有这许多可以现成使用的包,但是代价是:你需要检查这些包本身是否存在安全问题。

幸运的是Node Security project(nsp)是一个非常棒的工具,来检查你使用的模块是否是易被一些已知的手段攻击的。

npm i nsp -g  
# either audit the shrinkwrap
nsp audit-shrinkwrap  
# or the package.json
nsp audit-package  

最后

这个清单主要根据OWASP维护的Web Application Security Testing Cheat Sheet所列。

原文链接

https://blog.risingstack.com/node-js-security-checklist/

查看原文

silenceboy 收藏了文章 · 2019-04-08

Koa2 中间件原理解析 —— 看了就会写

在这里插入图片描述


阅读原文


前言

Koa 2.x 版本是当下最流行的 NodeJS 框架,Koa 2.0 的源码特别精简,不像 Express 封装的功能那么多,所以大部分的功能都是由 Koa 开发团队(同 Express 是一家出品)和社区贡献者针对 Koa 对 NodeJS 的封装特性实现的中间件来提供的,用法非常简单,就是引入中间件,并调用 Koause 方法使用在对应的位置,这样就可以通过在内部操作 ctx 实现一些功能,我们接下来就讨论常用中间件的实现原理以及我们应该如何开发一个 Koa 中间件供自己和别人使用。


Koa 的洋葱模型介绍

我们本次不对洋葱模型的实现原理进行过多的刨析,主要根据 API 的使用方式及洋葱模型分析中间件是如何工作的。

// 洋葱模型特点
// 引入 Koa
const Koa = require("koa");

// 创建服务
const app = new Koa();

app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(2);
});

app.use(async (ctx, next) => {
    console.log(3);
    await next();
    console.log(4);
});

app.use(async (ctx, next) => {
    console.log(5);
    await next();
    console.log(6);
});

// 监听服务
app.listen(3000);

// 1
// 3
// 5
// 6
// 4
// 2

我们知道 Koause 方法是支持异步的,所以为了保证正常的按照洋葱模型的执行顺序执行代码,需要在调用 next 的时候让代码等待,等待异步结束后再继续向下执行,所以我们在 Koa 中都是建议使用 async/await 的,引入的中间件都是在 use 方法中调用,由此我们可以分析出每一个 Koa 的中间件都是返回一个 async 函数的。


koa-bodyparser 中间件模拟

想要分析 koa-bodyparser 的原理首先需要知道用法和作用,koa-bodyparser 中间件是将我们的 post 请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body 上,方便我们在其他中间件或接口处取值,使用前需提前安装。

npm install koa koa-bodyparser

koa-bodyparser 具体用法如下:

// koa-bodyparser 的用法
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");

const app = new Koa();

// 使用中间件
app.use(bodyParser());

app.use(async (ctx, next) => {
    if (ctx.path === "/" && ctx.method === "POST") {
        // 使用中间件后 ctx.request.body 属性自动加上了 post 请求的数据
        console.log(ctx.request.body);
    }
});

app.listen(3000);

根据用法我们可以看出 koa-bodyparser 中间件引入的其实是一个函数,我们把它放在了 use 中执行,根据 Koa 的特点,我们推断出 koa-bodyparser 的函数执行后应该给我们返回了一个 async 函数,下面是我们模拟实现的代码。

// 文件:my-koa-bodyparser.js
const querystring = require("querystring");

module.exports = function bodyParser() {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            // 存储数据的数组
            let dataArr = [];

            // 接收数据
            ctx.req.on("data", data => dataArr.push(data));

            // 整合数据并使用 Promise 成功
            ctx.req.on("end", () => {
                // 获取请求数据的类型 json 或表单
                let contentType = ctx.get("Content-Type");

                // 获取数据 Buffer 格式
                let data = Buffer.concat(dataArr).toString();

                if (contentType === "application/x-www-form-urlencoded") {
                    // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body
                    ctx.request.body = querystring.parse(data);
                } else if (contentType === "applaction/json") {
                    // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
                    ctx.request.body = JSON.parse(data);
                }

                // 执行成功的回调
                resolve();
            });
        });

        // 继续向下执行
        await next();
    };
};

在上面代码中由几点是需要我们注意的,即 next 的调用以及为什么通过流接收数据、处理数据和将数据挂在 ctx.request.body 要在 Promise 中进行。

首先是 next 的调用,我们知道 Koanext 执行,其实就是在执行下一个中间件的函数,即下一个 use 中的 async 函数,为了保证后面的异步代码执行完毕后再继续执行当前的代码,所以我们需要使用 await 进行等待,其次就是数据从接收到挂在 ctx.request.body 都在 Promise 中执行,是因为在接收数据的操作是异步的,整个处理数据的过程需要等待异步完成后,再把数据挂在 ctx.request.body 上,可以保证我们在下一个 useasync 函数中可以在 ctx.request.body 上拿到数据,所以我们使用 await 等待一个 Promise 成功后再执行 next


koa-better-body 中间件模拟

koa-bodyparser 在处理表单提交时还是显得有一点弱,因为不支持文件上传,而 koa-better-body 则弥补了这个不足,但是 koa-better-bodyKoa 1.x 版本的中间件,Koa 1.x 的中间件都是使用 Generator 函数实现的,我们需要使用 koa-convertkoa-better-body 转化成 Koa 2.x 的中间件。

npm install koa koa-better-body koa-convert path uuid

koa-better-body 具体用法如下:

// koa-better-body 的用法
const Koa = require("koa");
const betterBody = require("koa-better-body");
const convert = require("koa-convert"); // 将  koa 1.0 中间转化成 koa 2.0 中间件
const path = require("path");
const fs = require("fs");
const uuid = require("uuid/v1"); // 生成随机串

const app = new Koa();

// 将 koa-better-body 中间件从 koa 1.0 转化成 koa 2.0,并使用中间件
app.use(convert(betterBody({
    uploadDir: path.resolve(__dirname, "upload")
})));

app.use(async (ctx, next) => {
    if (ctx.path === "/" && ctx.method === "POST") {
        // 使用中间件后 ctx.request.fields 属性自动加上了 post 请求的文件数据
        console.log(ctx.request.fields);

        // 将文件重命名
        let imgPath = ctx.request.fields.avatar[0].path;
        let newPath = path.resolve(__dirname, uuid());
        fs.rename(imgPath, newPath);
    }
});

app.listen(3000);

上面代码中 koa-better-body 的主要功能就是将表单上传的文件存入本地指定的文件夹下,并将文件流对象挂在了 ctx.request.fields 属性上,我们接下来就模拟 koa-better-body 的功能实现一版基于 Koa 2.x 处理文件上传的中间件。

// 文件:my-koa-better-body.js
const fs = require("fs");
const uuid = require("uuid/v1");
const path = require("path");

// 给 Buffer 扩展 split 方法预备后面使用
Buffer.prototype.split = function (sep) {
    let len = Buffer.from(sep).length; // 分隔符所占的字节数
    let result = []; // 返回的数组
    let start = 0; // 查找 Buffer 的起始位置
    let offset = 0; // 偏移量

    // 循环查找分隔符
    while ((offset = this.indexOf(sep, start)) !== -1) {
        // 将分隔符之前的部分截取出来存入
        result.push(this.slice(start, offset));
        start = offset + len;
    }

    // 处理剩下的部分
    result.push(this.slice(start));

    // 返回结果
    return result;
}

module.exports = function (options) {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            let dataArr = []; // 存储读取的数据

            // 读取数据
            ctx.req.on("data", data => dataArr.push(data));

            ctx.req.on("end", () => {
                // 取到请求体每段的分割线字符串
                let bondery = `--${ctx.get("content-Type").split("=")[1]}`;

                // 获取不同系统的换行符
                let lineBreak = process.platform === "win32" ? "\r\n" : "\n";

                // 非文件类型数据的最终返回结果
                let fields = {};

                // 分隔的 buffer 去掉没用的头和尾即开头的 '' 和末尾的 '--'
                dataArr = dataArr.split(bondery).slice(1, -1);

                // 循环处理 dataArr 中每一段 Buffer 的内容
                dataArr.forEach(lines => {
                    // 对于普通值,信息由包含键名的行 + 两个换行 + 数据值 + 换行组成
                    // 对于文件,信息由包含 filename 的行 + 两个换行 + 文件内容 + 换行组成
                    let [head, tail] = lines.split(`${lineBreak}${lineBreak}`);

                    // 判断是否是文件,如果是文件则创建文件并写入,如果是普通值则存入 fields 对象中
                    if (head.includes("filename")) {
                        // 防止文件内容含有换行而被分割,应重新截取内容并去掉最后的换行
                        let tail = lines.slice(head.length + 2 * lineBreak.length, -lineBreak.length);

                        // 创建可写流并指定写入的路径:绝对路径 + 指定文件夹 + 随机文件名,最后写入文件
                        fs.createWriteStream(path.join(__dirname, options.uploadDir, uuid())).end(tail);
                    } else {
                        // 是普通值取出键名
                        let key = head.match(/name="(\w+)"/)[1];

                        // 将 key 设置给 fields tail 去掉末尾换行后的内容
                        fields[key] = tail.toString("utf8").slice(0, -lineBreak.length);
                    }
                });

                // 将处理好的 fields 对象挂在 ctx.request.fields 上,并完成 Promise
                ctx.request.fields = fields;
                resolve();
            });
        });

        // 向下执行
        await next();
    }
}

上面的内容逻辑可以通过代码注释来理解,就是模拟 koa-better-body 的功能逻辑,我们主要的关心点在于中间件实现的方式,上面功能实现的异步操作依然是读取数据,为了等待数据处理结束仍然在 Promise 中执行,并使用 await 等待,Promise 执行成功调用 next


koa-views 中间件模拟

Node 模板是我们经常使用的工具用来在服务端帮我们渲染页面,模板的种类繁多,因此出现了 koa-view 中间件,帮我们来兼容这些模板,先安装依赖的模块。

npm install koa koa-views ejs

下面是一个 ejs 的模板文件:

<!-- 文件:index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ejs</title>
</head>
<body>
    <%=name%>
    <%=age%>

    <%if (name=="panda") {%>
        panda
    <%} else {%>
        shen
    <%}%>

    <%arr.forEach(item => {%>
        <li><%=item%></li>
    <%})%>
</body>
</html>

koa-views 具体用法如下:

// koa-views 的用法
const Koa = require("koa");
const views = require("koa-views");
const path = require("path");

const app = new Koa();

// 使用中间件
app.use(views(path.resolve(__dirname, "views"), {
    extension: "ejs"
}));

app.use(async (ctx, next) => {
    await ctx.render("index", { name: "panda", age: 20, arr: [1, 2, 3] });
});

app.listen(3000);

可以看出我们使用了 koa-views 中间件后,让 ctx 上多了 render 方法帮助我们实现对模板的渲染和响应页面,就和直接使用 ejs 自带的 render 方法一样,并且从用法可以看出 render 方法是异步执行的,所以需要使用 await 进行等待,接下来我们就来模拟实现一版简单的 koa-views 中间件。

// 文件:my-koa-views.js
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");

// 将读取文件方法转换成 Promise
const readFile = promisify(fs.radFile);

// 到处中间件
module.exports = function (dir, options) {
    return async (ctx, next) => {
        // 动态引入模板依赖模块
        const view = require(options.extension);

        ctx.render = async (filename, data) => {
            // 异步读取文件内容
            let tmpl = await readFile(path.join(dir, `${filename}.${options.extension}`), "utf8");

            // 将模板渲染并返回页面字符串
            let pageStr = view.render(tmpl, data);

            // 设置响应类型并响应页面
            ctx.set("Content-Type", "text/html;charset=utf8");
            ctx.body = pageStr;
        }

        // 继续向下执行
        await next();
    }
}

挂在 ctx 上的 render 方法之所以是异步执行的是因为内部读取模板文件是异步执行的,需要等待,所以 render 方法为 async 函数,在中间件内部动态引入了我们使的用模板,如 ejs,并在 ctx.render 内部使用对应的 render 方法获取替换数据后的页面字符串,并以 html 的类型响应。


koa-static 中间件模拟

下面是 koa-static 中间件的用法,代码使用的依赖如下,使用前需安装。

npm install koa koa-static mime

koa-static 具体用法如下:

// koa-static 的用法
const Koa = require("koa");
const static = require("koa-static");
const path = require("path");

const app = new Koa();

app.use(static(path.resolve(__dirname, "public")));

app.use(async (ctx, next) => {
    ctx.body = "hello world";
});

app.listen(3000);

通过使用和分析,我们知道了 koa-static 中间件的作用是在服务器接到请求时,帮我们处理静态文件,如果我们直接访问文件名的时候,会查找这个文件并直接响应,如果没有这个文件路径会当作文件夹,并查找文件夹下的 index.html,如果存在则直接响应,如果不存在则交给其他中间件处理。

// 文件:my-koa-static.js
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const { promisify } = require("util");

// 将 stat 和 access 转换成 Promise
const stat = promisify(fs.stat);
const access = promisify(fs.access)

module.exports = function (dir) {
    return async (ctx, next) => {
        // 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 /
        let realPath = path.join(dir, ctx.path);

        try {
            // 获取 stat 对象
            let statObj = await stat(realPath);

            // 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html
            if (statObj.isFile()) {
                ctx.set("Content-Type", `${mime.getType()};charset=utf8`);
                ctx.body = fs.createReadStream(realPath);
            } else {
                let filename = path.join(realPath, "index.html");

                // 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理
                await access(filename);

                // 存在设置文件类型并响应内容
                ctx.set("Content-Type", "text/html;charset=utf8");
                ctx.body = fs.createReadStream(filename);
            }
        } catch (e) {
            await next();
        }
    }
}

上面的逻辑中需要检测路径是否存在,由于我们导出的函数都是 async 函数,所以我们将 stataccess 转化成了 Promise,并用 try...catch 进行捕获,在路径不合法时调用 next 交给其他中间件处理。


koa-router 中间件模拟

Express 框架中,路由是被内置在了框架内部,而 Koa 中没有内置,是使用 koa-router 中间件来实现的,使用前需要安装。

npm install koa koa-router

koa-router 功能非常强大,下面我们只是简单的使用,并且根据使用的功能进行模拟。

// koa-router 的简单用法
const Koa = require("Koa");
const Router = require("koa-router");

const app = new Koa();
const router = new Router();

router.get("/panda", (ctx, next) => {
    ctx.body = "panda";
});

router.get("/panda", (ctx, next) => {
    ctx.body = "pandashen";
});

router.get("/shen", (ctx, next) => {
    ctx.body = "shen";
})

// 调用路由中间件
app.use(router.routes());

app.listen(3000);

从上面看出 koa-router 导出的是一个类,使用时需要创建一个实例,并且调用实例的 routes 方法将该方法返回的 async 函数进行连接,但是在匹配路由的时候,会根据路由 get 方法中的路径进行匹配,并串行执行内部的回调函数,当所有回调函数执行完毕之后会执行整个 Koa 串行的 next,原理同其他中间件,我下面来针对上面使用的功能简易实现。

// 文件:my-koa-router.js
// 控制每一个路由层的类
class Layer {
    constructor(path, cb) {
        this.path = path;
        this.cb = cb;
    }
    match(path) {
        // 地址的路由和当前配置路由相等返回 true,否则返回 false
        return path === this.path;
    }
}

// 路由的类
class Router {
    constructor() {
        // 存放每个路由对象的数组,{ path: /xxx, fn: cb }
        this.layers = [];
    }
    get(path, cb) {
        // 将路由对象存入数组中
        this.layers.push(new Layer(path, cb));
    }
    compose(ctx, next, handlers) {
        // 将匹配的路由函数串联执行
        function dispatch(index) {
            // 如果当前 index 个数大于了存储路由对象的长度,则执行 Koa 的 next 方法
            if(index >= handlers.length) return next();

            // 否则调用取出的路由对象的回调执行,并传入一个函数,在传入的函数中递归 dispatch(index + 1)
            // 目的是为了执行下一个路由对象上的回调函数
            handlers[index].cb(ctx, () => dispatch(index + 1));
        }

        // 第一次执行路由对象的回调函数
        dispatch(0);
    }
    routes() {
        return async (ctx, next) { // 当前 next 是 Koa 自己的 next,即 Koa 其他的中间件
            // 筛选出路径相同的路由
            let handlers = this.layers.filter(layer => layer.match(ctx.path));
            this.compose(ctx, next, handlers);
        }
    }
}

在上面我们创建了一个 Router 类,定义了 get 方法,当然还有 post 等,我们只实现 get 意思一下,get 内为逻辑为将调用 get 方法的参数函数和路由字符串共同构建成对象存入了数组 layers,所以我们创建了专门构造路由对象的类 Layer,方便扩展,在路由匹配时我们可以根据 ctx.path 拿到路由字符串,并通过该路由过滤调数组中与路由不匹配的路由对象,调用 compose 方法将过滤后的数组作为参数 handlers 传入,串行执行路由对象上的回调函数。

compose 这个方法的实现思想非常的重要,在 Koa 源码中用于串联中间件,在 React 源码中用于串联 reduxpromisethunklogger 等模块,我们的实现是一个简版,并没有兼容异步,主要思想是递归 dispatch 函数,每次取出数组中下一个路由对象的回调函数执行,直到所有匹配的路由的回调函数都执行完,执行 Koa 的下一个中间件 next,注意此处的 next 不同于数组中回调函数的参数 next,数组中路由对象回调函数的 next 代表下一个匹配路由的回调。


总结

上面我们分析和模拟了一些中间件,其实我们会理解 KoaExpress 相比较的优势是没有那么繁重,开发使用方便,需要的功能都可以用对应的中间件来实现,使用中间件可以给我们带来一些好处,比如能将我们处理好的数据和新方法挂载在 ctx 上,方便后面 use 传入的回调函数中使用,也可以帮我们处理一些公共逻辑,不至于在每一个 use 的回调中都去处理,大大减少了冗余代码,由此看来其实给 Koa 使用中间件的过程就是一个典型的 “装饰器” 模式,在通过上面的分析之后相信大家也了解了 Koa 的 “洋葱模型” 和异步特点,知道该如何开发自己的中间件了。


查看原文

silenceboy 发布了文章 · 2019-04-02

关于js的浅拷贝与深拷贝

原文地址:http://www.silenceboy.com/201...

浅拷贝和深拷贝只针对像Object, Array这样的复杂对象的.简单来说,浅拷贝只拷贝一层对象的属性,而深拷贝则递归拷贝了所有层级。

浅拷贝

通过 Object.assign 来实现浅拷贝。

let a = {
    num: 1
}
let b = Object.assign({}, a)
a.num = 2
console.log(b.num) // 1

通过展开运算符(…)来实现浅拷贝

let a = {
    num: 1
}
let b = {...a}
a.num = 2
console.log(b.num) // 1

通过属性赋值来实现浅拷贝:

const obj = { a:1, arr: [2,3] };
const shallowObj = shallowCopy(obj);

function shallowCopy(src) {
  var dst = {};
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      dst[prop] = src[prop];
    }
  }
  return dst;
}

该方法体现了浅拷贝的问题.因为浅拷贝只会将对象的各个属性进行依次拷贝,并不会进行递归拷贝,而 JavaScript 存储对象都是存地址的,所以浅拷贝会导致 obj.arr 和 shallowObj.arr 指向同一块内存地址.

导致的结果就是:

shallowObj.arr[1] = 5;
obj.arr[1]   // = 5

这种情况就需要用到深拷贝了.

深拷贝

通过JSON序列化实现深拷贝

你不知道的JavaScript(上)
许多JavaScript框架都提出了自己的解决办法,但是Javascript应该采用那种方法作为标准呐? 在很长一段时间里,这个问题都没有明确的答案.对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse(JSON.stringify(someObj));

当然,这种方法需要保证对象是JSON安全的,所以只适用于部分情况.

该方法的局限性:

  1. 会忽略 undefined
  2. 会忽略 symbol
  3. 不能序列化函数
  4. 不能解决循环引用的对象

递归完成深拷贝

function deepCopy(obj){
    //判断是否是简单数据类型,
    if(typeof obj == "object"){
        //复杂数据类型
        var result = obj.constructor == Array ? [] : {};
        for(let i in obj){
            result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
    }else {
        //简单数据类型 直接 == 赋值
        var result = obj;
    }
    return result;
}
查看原文

赞 0 收藏 0 评论 0

silenceboy 发布了文章 · 2019-03-23

关SSL_ERROR_SYSCALL in connection to github.com:443错误的两种解决方案

文章来源: http://www.silenceboy.com/201...

错误来源

使用使用hexo部署博客是,遇到以下错误:

fatal: unable to access 'https://github.com/silenceboychen/silenceboychen.github.io.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443
FATAL Something's wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.html
Error: Spawn failed
    at ChildProcess.<anonymous> (/Users/chenhao/new_start/nodejs/blog/node_modules/hexo-util/lib/spawn.js:52:19)
    at ChildProcess.emit (events.js:182:13)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:239:12)

两种解决方案:

方案一

取消http代理:

$ git config --global --unset http.proxy
$ git config --global --unset https.proxy

设置env GIT_SSL_NO_VERIFYtrue然后再次部署:

$ env GIT_SSL_NO_VERIFY=true hexo d

问题解决。

方案二

在hexo项目的根目录下的_config.yml文件中把仓库链接地址由https修改为ssh的地址。

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 154 次点赞
  • 获得 15 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 12 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-12-12
个人主页被 914 人浏览