【译】如何在 Node.js 中创建安全的 GraphQL API

2020-06-28 11:26:56 浏览数 (1)

原文地址:How to Create a Secure Node.js GraphQL API 作者:Marcos

本文的目的是提供一份快速指南 -- 《如何快速在如何在 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 是一种 APIs 查询语言,开发用于 Facebook 内部使用,并在 2015 年发布供公众使用。它支持读取、写入和实时更新。同时它也是开源的,常常被用来和 REST 或者其他的架构来比较。简而言之,它基于两部分:

  • GraphQL Queries(查询):允许客户端进行读取和操作,并可以指定数据的接收格式
  • GraphQL Mutations(变更):向服务端写入数据,可以约定数据的写入方式。

尽管本文应该以一个真实简单的场景来演示说明如何构建和使用 GraphQL APIs,但我们不会对 GraphQL 进行详细的介绍。原因很简单,因为 GraphQL 官方团队提供的文档非常全,并且在 Introduction to GraphQL 列出了几个最佳实践。

什么是 GraphQL 查询?

如前面所讲述的那样,查询 (query) 是客户端从 API 读取和操作数据的方式。你可以传递一个对象的类型,并且定义所希望返回的字段类型。下面是一个简单的查询:

代码语言:javascript复制
query{
  users{
    firstName,
    lastName
  }
}

在这个查询中,我们想从用户集合中获取所有的用户,但只需要返回 firstNamelastName。查询结果将会像下面这样:

代码语言:javascript复制
{
  "data": {
    "users": [
      {
        "firstName": "Marcos",
        "lastName": "Silva"
      },
      {
        "firstName": "Paulo",
        "lastName": "Silva"
      }
    ]
  }
}

这对于客户端使用来说非常简单。

使用 GraphQL API 的目的是什么?

构建 API 的目的是希望能将软件作为一种服务,并可以被其他外部服务集成。即使你的这个应用只提供给了一个前端使用,你也可以将这个前端视为一个外部服务。所以当两者通过 API 的形式来进行通信时,其他项目也可以使用同样的方式来工作。

如果你在一个大团队中工作,可以将它拆分成前端和后端两个团队,这样他们就可以使用相同的技术栈来工作从而提高效率。在构建 API 时,选择更接近实际需求的解决方案非常重要。

在本文中,我们将关注与怎么使用框架来构建 GraphQL API。

GraphQL 比 REST 更好吗?

这个问题可能会有点「引战」,但不得不说,这得看情况来选择。

GraphQL 在一些场景中非常适合。REST 是一种架构设计模式,在很多场景中也得到了验证。如今,有大量的文章试图证明为什么一个比另一个好,或者你应该使用 REST 而不是 GraphQL。此外,有很多方法在内部使用 GraphQL,并仍然用 REST 来维护你的 API。

最好的方法是去了解每种方法的优点,分析可使用的解决方案,评估团队使用这种解决方案的舒适程度,并且评估你和你的团队成员是否可以快速上手。

这篇文章更多的是一个实用指南,而不是对 GraphQL 和 REST 进行主观比较。如果你想了解这两者的详细区别,我建议你可以看看我的另一篇文章: GraphQL vs. REST - A GraphQL Tutorial。

为什么使用 Node.js?

GraphQL 有几种不同的库可供我们实用。出于本文的目的,我们决定实用 JavaScript 和 Node.js,因为它们被广泛地使用,并且 Node.js 允许开发者使用熟悉的前端语言来进行服务端开发。

将我们的构建方式和基于 REST 的 API 进行比较非常有用,类似另外一篇文章那样:Creating a Secure REST API in Node.js. 这篇文章还展示了如何使用 Node.js 和 Express 来开发 REST API 框架,你可以在这两种方法中找出一些差异。Node.js 还设计了一些可扩展的网络应用程序,包括一个全球性的社区以及几个开源库,你可以在 npm 上找到他们。

接下来,我们将演示如何使用 GraphQL、Node.js 和 Express 来构建 API !

准备开始 GraphQL

我们会先为 GraphQL API 提供一个构思。在这之前,你需要了解 Node.js 和 Express 的基础知识。本文的所有示例可以在这个链接中获得:https://github.com/makinhs/node-graphql-tutorial

我们将会处理两种类型的资源(两个集合):

  • Users:用来展示如何进行基本的 CRUD。
  • Products:通过一些细节来展示 GraphQL 的更多功能。

Users 的数据结构如下:

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

Products 的数据结构如下:

  • id
  • name
  • description
  • price

至于语言版本,我们将会在这个项目中使用 TypeScript。在源文件中,你可以使用 TypeScript 来修改所有的内容。

Let’s Code!

首先,确保你的 Node.js 版本是最新的。撰写本文时,Node.js 当前的版本为 10.15.3。

初始化项目

我们先创建一个名为 node-graphql 的文件夹。然后我们打开一个终端或者 git 控制台,并使用 npm init 来初始化。

配置项目依赖和 TypeScript

为了加快这一步,你可以直接使用我们 git 仓库中的内容来替换你的 package.json,这里面包含了所需的所有依赖:

代码语言:javascript复制
{
  "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。它将会为你安装 GraphQL API、Express 的所有依赖。

下一步是配置 TypeScript 的编译模式,我们在项目根目录下创建一个 tsconfig.json,并输入以下内容:

代码语言:javascript复制
{
  "compilerOptions": {
    "target": "ES2016",
    "module": "commonjs",
    "outDir": "./build",
    "strict": true,
    "esModuleInterop": true
  }
}

这个配置将会在当前应用文件夹中生效,我们可以创建一个 app.ts,并输入一些调试代码:

代码语言:javascript复制
console.log('Hello Graphql Node API tutorial');

通过我们的配置,现在你可以在终端中运行 npm start,等待构建完成后会发现代码正常执行。在控制台中应该会看到我们打印的内容 Hello Graphql Node API tutorial。在后台,会根据 tsconfig.json 来将 TypeScript 编译成纯 JavaScript,然后会执行 build 文件夹中的构建结果。

现在我们来构建 GraphQL API 的基本内容,首先我们先导入以下依赖库:

代码语言:javascript复制
import express from 'express';
import graphqlHTTP from 'express-graphql';
import { makeExecutableSchema } from 'graphql-tools';

下一步是在 Express 中处理我们的应用逻辑和基本的 GraphQL 配置,例如:

代码语言:javascript复制
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 端口
  • 定义这个例子会使用到的 queries 和 mutations
  • 定义 queries 和 mutations 如何工作

好,那 typeDefs 和 resolvers 是什么?queries 和 mutations 之间有什么关系?

  • typeDefs:定义 queries 和 mutations 的数据结构
  • resolvers:在这里定义了 queries 和 mutations 是如何工作的,而不是定义所期望的字段
  • Queries(查询):我们要从服务器获取的内容
  • Mutations(变更):请求将会改变服务器中的数据

现在,我们重新执行一下 npm start,我们可以看到在控制台中显示了以下消息:Node Graphql API listening on port 3000!

现在我们可以尝试通过以下方式来调试我们的 GraphQL 应用程序:

http://localhost:3000/graphql

toptal-blog-image-1556642146731-899e2ac152384d9eb080d40467351d7c.png

很好,我们现在可以写我们的第一个查询了:

toptal-blog-image-1556642154647-6d4f0a1557da7ba03eb98f1daa8e451a.png

需要注意的是要按照我们在 typeDefs 中定义的方式,这个页面可以帮我们构建查询。

那么我们怎么才能改变这个值呢?可以用 Mutations!

现在,我们来看看当我们用一个 mutation(变更) 来改变内存里的一个值会发安生什么:

toptal-blog-image-1556642161175-73ed2ee503125a93d3dbc04d9e292407.png

我们现在可以通过 GraphQL Node.js API 来做基本的 CRUD 了。接下来我们继续。

Products

我们将使用一个名为 Products 的模块。为了简化本文的篇幅,我们将使用内存数据库来进行演示。接下来我们将定义 Products 的模型(model)和服务(service)。

模型(model)定义如下:

代码语言:javascript复制
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 进行通信的服务(service)定义如下:

代码语言:javascript复制
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。模型的定义:

代码语言:javascript复制
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;
    }

}

同时,服务的定义如下:

代码语言:javascript复制
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;
        };

    }

}

小提示:源代码可以从这个链接中获得。

现在我们可以运行和测试我们的代码了。执行 npm start,我们将在 3000 端口运行我们的服务器。我们可以通过这个地址来访问 GraphQL 进行调试: http://localhost:3000/graphql 。

我们试一下用 mutation 来将一个 item 添加到 Product 列表中:

toptal-blog-image-1556642172745-bf28c918810cce73d3fae42d55bc1aa2.png

为了验证是否正常,我们来对 Products 进行查询,但我们只希望返回 idnameprice

代码语言:javascript复制
query{
  products{
    id,
    name,
    price
  }
}

查询结果如下:
{
  "data": {
    "products": [
          {
        "id": 100,
        "name": "My amazing product",
        "price": 400
      }
    ]
  }
}

上面的代码的执行结果会如你所愿。你还可以更改你所需要的字段,比如你希望添加 description

代码语言:javascript复制
query{
  products{
    id,
    name,
    description,
    price
  }
}

我们试下对 Users 发起变更:

代码语言:javascript复制
mutation{
  user(id:200,
  firstName:"Marcos",
  lastName:"Silva",
  password:"amaz1ingP4ss",
  permissionLevel:9,
  email:"marcos.henrique@toptal.com") {
    id
  }
}

然后进行查询:

代码语言:javascript复制
query{
  users{
    id,
    firstName,
    lastName,
    password,
    email
  }
}

会得到如下内容:

代码语言:javascript复制
{
  "data": {
    "users": [
      {
        "id": 200,
        "firstName": "Marcos",
        "lastName": "Silva",
        "password": "kpj6Mq0tGChGbZ BT9Nw6RMCLReZEPPyBCaUS3X23lZwCCp1Ogb94/oqJlya0xOBdgEbUwqRSuZRjZGhCzLdeQ==",
        "email": "marcos.henrique@toptal.com"
      }
    ]
  }
}

到现在,我们的 GraphQL 的基本框架已经搭好了!如果还要继续构建成一个有用的、功能全的 API 还有许多工作要做,但现在基本的核心已经搭好了。

总结与最后的想法

即使已经尽量减少篇幅了,这篇文章还是很长,因为包含了许多关于开发 GraphQL Node.js API 的基本信息。

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

  • 使用 Node Express 和 GraphQL 来构建 GraphQL API;
  • GraphQL 的基本使用;
  • 查询 (Query) 和变更 (Mutation) 的基本使用;
  • 创建模块 (Module) 的基本方法;
  • 测试我们的 GraphQL API;

为了将内容侧重于开发使用,本文忽略了开发中一些重要的内容,简单总结如下:

  • 新增内容时需要校验
  • 对服务中的错误进行正确处理
  • 校验用户在每个请求中所使用的字段
  • 添加一个 JWT 拦截器来保护 API 接口
  • 用更有效的加密算法来处理密码
  • 添加单元和集成测试

请记住,我们在 Git 上有完整的源代码。请随意使用、fork、提 issue 和 PR。请注意,本文中所提到所有标准和建议都不会是一成不变的。

这只是许多构建 GraphQL API 方法中的一种。另外,一定要详细地阅读和探索学习 GraphQL,并了解它能给我们带来什么,怎么可以让我们的 API 接口设计地更好。

0 人点赞