使用 JWT 在 Express.js 中实现用户身份验证

GraphQL 是传统 RESTful API 架构的流行替代方案,为 API 提供灵活高效的数据查询和操作语言。 随着其日益普及,优先考虑 GraphQL API 的安全性以保护应用程序免受未经授权的访问和潜在的数据泄露变得越来越重要。

保护 GraphQL API 的一种有效方法是实施 JSON Web 令牌 (JWT)。 JWT 提供了一种安全有效的方法来授予对受保护资源的访问权限并执行授权操作,从而确保客户端和 API 之间的安全通信。

GraphQL API 中的身份验证和授权

与 REST API 不同,GraphQL API 通常具有单个端点,允许客户端在查询中动态请求不同数量的数据。 虽然这种灵活性是其优势,但它也增加了潜在安全攻击的风险,例如破坏访问控制漏洞。

为了减轻这种风险,实施强大的身份验证和授权流程非常重要,包括正确定义访问权限。 通过这样做,您可以保证只有授权用户才能访问受保护的资源,并最终降低潜在安全漏洞和数据丢失的风险。

您可以在其中找到该项目的代码 GitHub 存储库。

设置 Express.js Apollo 服务器

阿波罗服务器 是广泛使用的 GraphQL API 的 GraphQL 服务器实现。 您可以使用它轻松构建 GraphQL 架构、定义解析器以及管理 API 的不同数据源。

要设置 Express.js Apollo 服务器,请创建并打开项目文件夹:

 mkdir graphql-API-jwt
cd graphql-API-jwt

接下来,运行以下命令以使用 Node 包管理器 npm 初始化新的 Node.js 项目:

 npm init --yes 

现在,安装这些软件包。

 npm install apollo-server graphql mongoose jsonwebtokens dotenv 

最后,在根目录中创建一个 server.js 文件,并使用以下代码设置服务器:

 const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();

const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req }) => ({ req }),
});

const MONGO_URI = process.env.MONGO_URI;

mongoose
  .connect(MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log("Connected to DB");
    return server.listen({ port: 5000 });
  })
  .then((res) => {
    console.log(`Server running at ${res.url}`);
  })
  .catch(err => {
    console.log(err.message);
  });

GraphQL 服务器使用 typeDefs 和解析器参数进行设置,指定 API 可以处理的架构和操作。 context 选项将 req 对象配置到每个解析器的上下文,这将允许服务器访问特定于请求的详细信息,例如标头值。

创建 MongoDB 数据库

要建立数据库连接,首先创建MongoDB数据库或在MongoDB Atlas上建立集群。 然后,复制提供的数据库连接 URI 字符串,创建一个 .env 文件,并输入连接字符串,如下所示:

 MONGO_URI="<mongo_connection_uri>"

定义数据模型

使用 Mongoose 定义数据模型。 创建一个新的 models/user.js 文件并包含以下代码:

 const {model, Schema} = require('mongoose');

const userSchema = new Schema({
    name: String,
    password: String,
    role: String
});

module.exports = model('user', userSchema);

定义 GraphQL 架构

在 GraphQL API 中,架构定义了可以查询的数据的结构,并概述了您可以执行以通过 API 与数据进行交互的可用操作(查询和变更)。

要定义模式,请在项目的根目录中创建一个新文件夹并将其命名为 graphql。 在此文件夹中,添加两个文件:typeDefs.js 和resolvers.js。

在 typeDefs.js 文件中,包含以下代码:

 const { gql } = require("apollo-server");

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    password: String!
    role: String!
  }
  input UserInput {
    name: String!
    password: String!
    role: String!
  }
  type TokenResult {
    message: String
    token: String
  }
  type Query {
    users: [User]
  }
  type Mutation {
    register(userInput: UserInput): User
    login(name: String!, password: String!, role: String!): TokenResult
  }
`;

module.exports = typeDefs;

为 GraphQL API 创建解析器

解析器函数确定如何响应客户端查询和突变以及架构中定义的其他字段来检索数据。 当客户端发送查询或突变时,GraphQL 服务器会触发相应的解析器来处理并返回来自各种来源(例如数据库或 API)所需的数据。

要使用 JSON Web 令牌 (JWT) 实现身份验证和授权,请为注册和登录突变定义解析器。 这些将处理用户注册和身份验证的过程。 然后,创建一个数据获取查询解析器,只有经过身份验证和授权的用户才能访问该解析器。

但首先,定义生成和验证 JWT 的函数。 在resolvers.js 文件中,首先添加以下导入。

 const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;

确保将用于对 JSON Web 令牌进行签名的密钥添加到 .env 文件中。

 SECRET_KEY = '<my_Secret_Key>'; 

要生成身份验证令牌,请包含以下函数,该函数还指定 JWT 令牌的唯一属性,例如过期时间。 此外,您还可以合并其他属性,例如根据您的特定应用程序要求适时发布的属性。

 function generateToken(user) {
  const token = jwt.sign(
   { id: user.id, role: user.role },
   secretKey,
   { expiresIn: '1h', algorithm: 'HS256' }
 );

  return token;
}

现在,实现令牌验证逻辑以验证后续 HTTP 请求中包含的 JWT 令牌。

 function verifyToken(token) {
  if (!token) {
    throw new Error('Token not provided');
  }

  try {
    const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
    return decoded;
  } catch (err) {
    throw new Error('Invalid token');
  }
}

此函数将接受令牌作为输入,使用指定的密钥验证其有效性,如果有效则返回解码后的令牌,否则抛出指示令牌无效的错误。

定义 API 解析器

要定义 GraphQL API 的解析器,您需要概述它将管理的具体操作,在本例中是用户注册和登录操作。 首先,创建一个解析器对象来保存解析器函数,然后定义以下突变操作:

 const resolvers = {
  Mutation: {
    register: async (_, { userInput: { name, password, role } }) => {
      if (!name || !password || !role) {
        throw new Error('Name password, and role required');
     }

      const newUser = new User({
        name: name,
        password: password,
        role: role,
      });

      try {
        const response = await newUser.save();

        return {
          id: response._id,
          ...response._doc,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Failed to create user');
      }
    },
    login: async (_, { name, password }) => {
      try {
        const user = await User.findOne({ name: name });

        if (!user) {
          throw new Error('User not found');
       }

        if (password !== user.password) {
          throw new Error('Incorrect password');
        }

        const token = generateToken(user);

        if (!token) {
          throw new Error('Failed to generate token');
        }

        return {
          message: 'Login successful',
          token: token,
        };
      } catch (error) {
        console.error(error);
        throw new Error('Login failed');
      }
    }
  },

注册突变通过将新用户数据添加到数据库来处理注册过程。 虽然登录突变管理用户登录,但在成功进行身份验证后,它将生成 JWT 令牌,并在响应中返回成功消息。

现在,包括用于检索用户数据的查询解析器。 为了确保只有经过身份验证和授权的用户才能访问此查询,请包含授权逻辑以将访问权限限制为仅具有管理员角色的用户。

本质上,查询将首先检查令牌的有效性,然后检查用户角色。 如果授权检查成功,解析器查询将继续从数据库获取并返回用户数据。

   Query: {
    users: async (parent, args, context) => {
      try {
        const token = context.req.headers.authorization || '';
        const decodedToken = verifyToken(token);

        if (decodedToken.role !== 'Admin') {
          throw new ('Unauthorized. Only Admins can access this data.');
        }

        const users = await User.find({}, { name: 1, _id: 1, role:1 });
        return users;
      } catch (error) {
        console.error(error);
        throw new Error('Failed to fetch users');
      }
    },
  },
};

最后,启动开发服务器:

 node server.js 

惊人的! 现在,继续使用浏览器中的 Apollo Server API 沙箱测试 API 的功能。 例如,您可以使用注册突变在数据库中添加新的用户数据,然后使用登录突变来验证用户。

最后,将 JWT 令牌添加到授权标头部分,然后继续查询数据库以获取用户数据。

保护 GraphQL API 的安全

身份验证和授权是保护 GraphQL API 的关键组件。 尽管如此,重要的是要认识到仅靠它们可能不足以确保全面的安全。 您应该实施额外的安全措施,例如输入验证和敏感数据加密。

通过采用全面的安全方法,您可以保护您的 API 免受不同的潜在攻击。