用Node.js构建一个安全的GraphQL API的方法

348 阅读9分钟

GraphQL通过验证和类型检查直接提供了安全性。然而,它并没有完全解决围绕API的安全问题。在这篇文章中,我们将学习如何通过使用Fastify和GraphQL构建一个简单的Node.js应用程序来保护GraphQL APIs。

根据其官方文档,GraphQL是一种用于API的图形查询语言,也是用我们的数据完成这些查询的运行时间。GraphQL对我们的API中的数据进行了清晰的描述,使我们能够创建快速和灵活的API,同时让客户完全控制描述他们需要的数据。像REST一样,GraphQL通过HTTP操作,所以它与数据库无关,可以与任何后端语言或客户端一起工作。

Fastify是一个基于插件的、高效的、性能超强的Node.js框架,适用于构建快速的HTTP服务器。受Hapi和Express的启发,Fastify提供了一个对开发者更友好、性能更好、开销更低的选择。

Fastify使用Mercurius插件支持GraphQL。Mercurius插件是Fastify的一个可配置的GraphQL适配器,我们将在随后的章节中进一步了解它。

让我们从先决条件开始。

前提条件

以下是本文的先决条件。

  • Node.js 12版或以上
  • JavaScript的基本知识
  • GraphQL的基本知识

开始使用

要开始,我们需要创建一个基本的Node.js服务器。

通过建立一个项目文件夹来创建一个启动项目,并从这个文件夹中,在命令行界面(CLI)中运行下面的代码。这将引导我们的应用程序并安装必要的依赖。

// bootstrap npm project
npm init -y

// install dependencies
npm i fastify nodemon fastify-plugin mercurius-auth jsonwebtoken

这个项目使用Mercurius的一个特定版本。要安装它,请运行以下命令。

npm i mercurius@7.9.1

接下来,我们通过在我们的package.json 文件中添加"type": "module" ,使ES6模块使用标准的JavaScript模块系统而不是commonJS。

然后,我们通过打开package.json 文件和编辑scripts部分来更新NPM脚本,以启动Node.js服务器的命令,如下所示。

"scripts": {
    // start the Node.js server in production
    "start": "node --es-module-specifier-resolution=node ./src/index.js",
    // use nodemon to restart development server when code is compiled
    "dev": "nodemon --es-module-specifier-resolution=node ./src/index.js"
}

注意,为了实现ES模块和Node的commonJS模块之间的互操作性,需要--es-module-specifier-resolution=node 这个片段。

现在,在根目录下创建一个src 目录。在 src 目录中,创建一个graphql 文件夹,包含一个schema.js 和一个resolvers.js 文件。将以下代码添加到schema.js 文件中。

const schema =`
  type Query {
    users: [User]!
  }
 
  type User {
    id: ID!
  }
  `;
  export default schema;

现在,将以下代码添加到resolvers.js 文件中。

const resolvers = {};
export default resolvers;

我们将在随后的章节中更新schema.js ,并添加resolvers.js 文件。但是,我们现在需要使用一些模板代码来创建它们,因为我们的服务器需要它们来正常工作。

在src目录下,创建一个包含以下代码的index.js 文件。

import fastify from 'fastify';
import mercurius from 'mercurius';
import jwt from 'jsonwebtoken';
import mercuriusAuth from 'mercurius-auth';
import schema from './graphql/schema.js'; 
import resolvers from './graphql/resolvers.js';

const port = process.env.PORT || 4500;
const app = fastify({ logger: true });

// Activate plugins below:
app.register(
  mercurius, { 
      schema, 
      resolvers, 
      graphiql: 'playground', 
      queryDepth: 7 
});

// register auth policy

// create server
const start = async () => {
  try {
    await app.listen(port);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};
start();

上面的代码创建了一个主要的Fastify服务器,并注册了Mercurius插件的选项。schema,resolvers,graphiql, 和queryDepth

现在我们可以通过运行npm run dev 来启动服务器。其输出结果如下。

{"level":30,"time":1620202591072,"pid":11775,"hostname":"pc-name","msg":"Server listening at https://127.0.0.1:4500"}

现在我们可以看到,我们的服务器正在工作。在下一节,我们将开始用GraphQL构建我们的博客API。

用Fastify和GraphQL建立一个安全的slog API

有几种不同的策略来保证API的安全。这些策略包括。

  • 认证和授权。认证是关于确认用户是他们所声称的人,而授权是关于权限。认证决定了一个用户是否可以登录,并在随后记住该用户。授权决定了分配给一个被识别的用户的权限,它规定了他们是否可以执行诸如创建、读取、更新或删除等操作。
  • 掩盖错误。隐瞒服务器错误的确切信息,以防止无意中向客户提供可能暴露服务器漏洞的细节。
  • 查询深度限制:为GraphQL查询指定一个最大深度。深度嵌套的查询是很危险的,因为它们对资源的要求很高,计算成本也很高。因此,它们会使我们的API崩溃。
  • 对输入进行消毒和验证。使用标准的网络安全技术来防止用户发送恶意数据。我们将在我们的应用程序中利用内置的GraphQL验证。

本文通过使用Mercurius和Mercurius Auth插件,用上述策略构建和保护我们的API。

对于我们的目的,Mercurius Auth插件有两个主要特点。首先,它使我们能够在我们的模式中的字段上定义自定义的授权指令。Auth指令是一些字符串,用来作为我们模式中受保护字段的标识符。

此外,它允许我们在进行GraphQL请求时将自定义的认证策略应用于这些受保护的字段。

我们从创建模拟数据开始。在src目录下,创建一个data文件夹,其中包含一个index.js文件,代码如下。

export default {
    users: [
        { id: 1, username: 'JohnDoe', email: 'John_doe@gmail.com', password: '12345', role: 'admin' },
        { id: 2, username: 'JaneDoe', email: 'Jane_doe@gmail.com', password: '12345', role: 'user' },
        { id: 3, username: 'JoeDoe', email: 'Joe_doe@gmail.com', password: '12345', role: 'user' }
    ]
};

接下来,我们通过用以下代码替换schema.js文件中的模板代码来设置我们的schema

const schema = `
 
directive @auth(
    requires: Role = ADMIN,
  ) on OBJECT | FIELD_DEFINITION
 
  enum Role {
    ADMIN
    USER
  }
 
type Query {
    user(id: ID!): User! @auth(requires: ADMIN)
    users: [User]! @auth(requires: ADMIN)
    login(username:String!, password:String!): String
}
 
type User {
    id: ID!
    username: String!
    email: String!
    password: String!
    role: String!
}
`;
 
export default schema;

我们在上面的代码中创建了我们的GraphQL模式,并为用户和users字段定义了auth指令。稍后,我们将对这些受保护的字段应用自定义策略。

现在,我们通过用以下代码替换resolvers.js 文件中的模板代码来添加我们的解析器。

import jwt from 'jsonwebtoken';
import Data from '../data';
 
const resolvers = {
    Query: {
        users: async (_, obj) => Data.users,
 
        user: async (_, { id }) => {
            let user = Data.users.find((user) => user.id == id);
            if (!user) {
                throw new Error('unknown user');
            }
            return user;
        },
 
        login: async (_, { username, password }) => {
            let user = Data.users.find((user) => user.username === username && user.password === password);
            if (!user) {
                throw new Error('unknown user!');
            }
 
            const token = jwt.sign({ username: user.username, password: user.password, role: user.role }, 'mysecrete');
            return token;
        }
    }
};
 
export default resolvers;

上面的代码包含了处理用户、用户和登录查询的解析器。

最后,我们必须通过注册我们的Mercurius Auth插件来添加一个自定义的auth策略。要做到这一点,在src目录下的index.js 文件中,我们在第19行的register auth policy 注释下面添加以下代码。

app.register(mercuriusAuth, {
    authContext(context) {
        return { identity: context.reply.request.headers['x-user'] };
    },
    async applyPolicy(authDirectiveAST, parent, args, context, info) {
        const token = context.auth.identity;
        try {
            const claim = jwt.verify(token, 'mysecrete');
        } catch (error) {
            throw new Error(`An error occurred. Try again!`);
        }
 
        return true;
    },
    authDirective: 'auth'
});

在我们上面的自定义策略中,authContext 方法检索头文件中的用户令牌,而applyPolicy 方法包含自定义的认证和授权策略。

另外,当用户认证或授权失败时,我们会抛出一个错误,并给出一个通用的消息,如 "发生了一个错误。再试一次!"这个消息的出现代替了向用户返回详细的服务器错误信息,这可能会暴露任何现有的服务器漏洞。

这样,我们的工作就完成了。我们将在下一节中测试我们的API。

测试API

首先,我们通过在根目录下运行npm run dev 来启动我们的服务器。接下来,我们从https://localhost:4500/playground 检索GraphQL游戏场

现在,当我们查询任何受保护的API,如usersuser ,我们得到一个错误,如下图所示。

因此,为了使我们的查询成功,我们必须对自己进行认证。让我们登录以获得一个令牌。

要登录,请在游乐场中打开一个新标签,并运行以下查询。

query {
  login(username: "JohnDoe", password: "12345")
}

如果查询成功,就会生成一个令牌并返回,如下图所示。

注意,用于登录的用户数据已经在数据目录中的index.js 文件中。

试图用不在该文件中的用户数据(如用户名:"John1Doe")登录,会导致一个错误,如下图所示。

现在,通过在头文件中传递我们的令牌(x-user ),我们可以成功地查询我们受保护的API,如下图所示。请确保复制从登录查询中收到的令牌,并将其值用于x-user

users 查询

这里是一个样本users 查询。

query {
  users {
    id
    username
    password
    email
    role
  }
}

添加你的令牌作为x-user HTTP头参数的值,如下图所示。

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

这将导致以下输出。

user 查询

下面是一个user 查询的例子。

query {
  user(id: "1") {
    id
    username
    email
    password
    role
  }
}

添加你的令牌作为x-user HTTP头参数的值,如下图所示。

{
  "x-user": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG5Eb2UiLCJwYXNzd29yZCI6IjEyMzQ1Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjQ0NTA3MDE5fQ.faslGjI6x-ODO2LGYOaTHClGs2MXCBOoMlWPYnwoH18"
}

这将导致以下的输出。

结论

在这篇文章中,我们发现通过使用Fastify和GraphQL来构建一个简单的Node.js应用程序,来确保GraphQL APIs的安全是多么容易。

正如所讨论的,GraphQL带有一些内置的安全性,如验证和类型检查。然而,使用户能够随意请求数据的灵活性和力量意味着安全应该始终是一个主要的关注点。

这篇文章还回顾了一些保护GraphQL APIs的策略。这些策略包括认证和授权、查询深度限制、掩盖错误、以及净化和验证输入。

这些安全策略是非常成功的,但我们可以通过实施其他方法来增加额外的安全性,如查询超时和速率限制,这些方法规定了客户端在每个时期可以查询API的频率。