前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【译】如何在 Node.js 中创建安全的 GraphQL API

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

作者头像
腾讯IVWEB团队
发布2020-06-28 11:26:56
2.5K0
发布2020-06-28 11:26:56
举报

原文地址: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 接口设计地更好。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 那什么是 GraphQL API?
  • 什么是 GraphQL 查询?
  • 使用 GraphQL API 的目的是什么?
  • GraphQL 比 REST 更好吗?
  • 为什么使用 Node.js?
  • 准备开始 GraphQL
  • Let’s Code!
    • 初始化项目
      • 配置项目依赖和 TypeScript
        • Products
          • Users
          • 总结与最后的想法
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档