所有文章 > 日积月累 > 为 GraphQL API 构建身份验证和授权的步骤
为 GraphQL API 构建身份验证和授权的步骤

为 GraphQL API 构建身份验证和授权的步骤

在构建 API 时,将所有数据提供给互联网上的每个人通常不是一个好主意。出于业务原因,我们需要检查谁是付费客户,出于安全和隐私原因,我们需要限制对我们系统部分内容的访问。

我们已经撰写了一篇有关使用 REST API 进行身份验证和授权的文章。

在本文中,我们将深入研究使用 GraphQL 的身份验证主题。

身份验证和授权之间的区别

两者经常与Auth一词混为一谈,例如“我们需要在系统中添加身份验证功能”。

虽然这两个主题相互交织,但它们涵盖不同的方面。

身份验证就是了解谁想用我们的系统做某事。

我们可以通过用户名/电子邮件和密码登录或使用 GitHub 或 Facebook 等社交登录来解决此问题。

授权就是知道他们被允许对我们的系统做什么。

这个问题更加复杂,因为它深深依赖于我们的业务案例。虽然不明显,但授权是业务逻辑,应该如此对待。

身份验证、授权和 GraphQL

我们应该把这个逻辑放在 GraphQL API 的哪里?

GraphQL 创建者的观点是“将授权逻辑委托给业务逻辑层” 。除了创建“完全水合的用户对象”而不是将身份验证令牌传递给我们的业务逻辑之外,他们对身份验证没有任何强烈的意见。

这是什么意思?我们的业务逻辑位于哪里?我们将在哪里创建这些用户对象?

GraphQL 系统的结构

GraphQL 系统的结构如下图所示:

GraphQL 体系结构

在左边,我们有不同类型的中间件,它们将一些逻辑应用于我们所有的请求。

中间部分是GraphQL 引擎,它利用 GraphQL Schema 确定如何将 GraphQL 查询转换为对正确解析器函数的调用。在大多数框架中,例如 Node.js 的 Express 框架,GraphQL 引擎也被实现为中间件。

在右侧,我们有不同类型的解析器函数,它们将 GraphQL 引擎与我们的业务逻辑连接起来。当查询相应的数据类型时,GraphQL 引擎会执行解析器。

业务逻辑可以是一个整体系统或一组微服务。

将身份验证集成到 GraphQL

授权请求,我们需要在业务逻辑中访问当前用户的身份。这要求我们将身份验证逻辑放到一个对所有请求都执行的位置。如果我们查看上面的图表,就会发现我们系统的中间件部分是执行该逻辑的最佳位置。

这种方法的另一个优点是:它完全独立于 GraphQL,因此我们可以重新使用我们的 REST 技能并重新应用它们!

将授权集成到 GraphQL

为了理解这一点,让我们看一个中间件、REST 资源处理程序函数和 GraphQL 解析器函数的简单实现。在这个例子中,我将使用 Node.js、Express 框架和 Apollo Server。

首先,我们的业务逻辑:

const getMessage = (id, user) => {
const message = dataStore[id];

if (message.recipient.id === user.id) return message;

if (message.sender.id === user.id) return message;

throw new Error("no permission");
};

它根据 ID 从内存存储中加载消息。如果用户是此消息的收件人或发件人,则允许他们阅读该消息。否则,我们会抛出错误。

二、认证中间件:

const authMiddleware = (request, response, next) => {
const token = request.get("authorization");
const user = loadUser(token);
if (user) {
request.user = user;
next();
}
response.status(401).end();
};

它从请求标头加载令牌并使用它来加载当前用户。如果成功,它会将此用户对象添加到请求中,以便稍后使用。

现在,让我们看一下 REST 实现。

expressApp.use(authMiddleware);

expressApp.get("/message/:id", (request, response) => {
try {
const message = getMessage(request.params.id, request.user);
response.end(message);
} catch (e) {
response.status(403).end({ error: e.message });
}
});

首先,我们使用我们的authMiddleware;这确保我们的端点处理程序中request有一个user对象。

然后,我们定义一个函数,用于向/messages/:id端点发送 GET 请求。它使用id参数和user我们的对象request来加载message业务逻辑函数。

如果一切顺利,我们将获得message并可以将其发送给客户端。如果不顺利,我们将 HTTP 状态设置为403(禁止)。

getMessage函数借助user对象来处理授权。它不关心它来自哪里。

接下来让我们看一个 GraphQL 实现,这里是用Apollo Server创建的。

expressApp.use(authMiddleware);

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

const typeDefs = gql`
type Query {
message(id: String!): String
}
`;

const resolvers = {
Query: {
message: (parent, args, request, info) =>
getMessage(args.id, request.user)
}
};

const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app: expressApp });

中间件的使用在这里保持不变。因此无需进行任何身份验证更改。

然后,我们使用该apollo-server-express模块定义一个仅具有Query类型的简单 GraphQL API message

然后我们定义解析器函数,它看起来与 REST 示例中的几乎相同。

解析器有四个参数,第三个参数是request我们已经从 REST 实现中知道的。由于我们的中间件使用此对象来附加user对象,因此我们可以像以前一样将其传递给我们的业务逻辑。

GraphQL 引擎id从我们的查询中解析参数并将其存储到第二个参数中,因此只有它的位置不同。

我们的业务逻辑功能处理授权,与 REST 实现相同。

这里的区别在于GraphQL 不了解 HTTP或其状态代码。如果它可以处理查询(即使结果为空),它也会直接使用200

我们的业务逻辑抛出的错误将被放置在errors响应主体中的数组内,并且客户端可以以某种方式处理它。

我们的响应messageJSON 将被设置为null,这使我们能够使用部分数据响应客户端的请求。例如,如果消息位于 GraphQL 响应树的某个深处。

{
"data": {
"message": null
},
"errors": [
{
"message": "no permission",
...
}
]
}

客户端处理

既然我们已经讨论了 API 中的身份验证和授权,我们还应该看看客户端的情况。

客户端身份验证

如果我们采用目前最流行的基于 token 的身份验证,那么首先需要获取这样的 token。我们可以通过两种方式来实现。

Auth 令牌检索

  1. 我们为注册和登录创建额外的 HTTP 端点
  2. 我们创建用于注册和登录的 GraphQL 突变

额外的 HTTP 端点将整个身份验证过程与 GraphQL API 分离。就像服务器端的中间件方法一样。如果执行身份验证的服务与我们的 API 不同,并且我们只需要来自它的令牌,那么这尤其有用。

GraphQL 突变感觉更加集成,但需要我们的 API 中有更多特殊情况,因为我们需要让用户以某种方式访问​​它们而无需经过身份验证。

我认为第一种方式风险较小。我们可以专注于 GraphQL API 的核心竞争力,并为未来保持灵活性。

代币存储和放置

令牌需要与我们的 GraphQL 请求一起发送到我们的 API。

为此,我们需要决定在哪里存储令牌以及在请求的哪里放置它。

存储的方式有两种,localStorageHTTP cookies。

当存储在 中时localStorage,我们可以通过 HTTP 标头发送令牌。当我们将其存储在 cookie 中时,我们会自动将其与 cookie 一起发送。

它与 REST API 相同,两种变体各有利弊。我不会详细介绍,但您可以在我们的REST 身份验证文章中阅读相关内容。

使用 Apollo 客户端的示例

让我们看一个简单的示例,即 Apollo Client,它是一个用 JavaScript 编写的独立于 UI 框架的 GraphQL 客户端库。

import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { ApolloLink, concat } from "apollo-link";

const httpLink = new HttpLink({ uri: "/graphql" });

const authMiddleware = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
authorization: localStorage.getItem("token") || null
}
});

return forward(operation);
});

const client = new ApolloClient({
link: concat(authMiddleware, httpLink)
});

来源:Apollo 客户端文档

这里我们使用localStorage/header 变体来存储和发送令牌。

Apollo 客户端有一个名为Apollo Link的网络层。它用于使不同的协议可插入。正如我们在 Express 示例的后端所看到的那样,链接被实现为中间件。

在此示例中,我们将 HTTP 中间件配置为我们的 GraphQL API 端点,然后创建一个自定义中间件/链接用于身份验证。

两者都插入ApolloClient构造函数,因此它们会执行我们发送的所有 GraphQL 查询。

自定义在每次请求之前authMiddleware进行检查,并将HTTP 标头设置为其找到的令牌。localStorageauthorization

到这里,我们已经看到了将身份验证保留在 GraphQL 之外的优点。我们可以fetch对 HTTP 端点进行简单调用以进行注册或登录,并在开始创建 GraphQL 客户端对象之前将收到的令牌存储在某个地方。

反过来也是一样。如果我们想注销用户,我们可以丢弃 GraphQL 客户端对象,而不需要以某种方式改变它。

客户端授权

如果我们正确设置了身份验证,API 现在就知道我们是我们所说的那个人,但仍然有可能我们想要查询我们无权访问的数据。

在客户端,就像在后端一样,授权是业务逻辑,因此我们需要考虑在具体情况下该怎么做。

我们有一个 UI,发送一个大的 GraphQL 查询来一次性获取所有数据,现在它返回了一些字段null和一些错误。

Apollo Client库为此定义了三种错误策略,我们可以为每个或所有请求设置其中一种。

fetch这些策略定义了我们可以在自定义客户端中或在无需任何特定 GraphQL 库的情况下直接使用的行为。

  1. 如果响应中存在任何 GraphQL 错误,我们就认为它失败了,并将其丢弃,就像发生网络错误一样。
  2. 我们忽略响应中的所有错误并尝试按原样使用数据。
  3. 我们尝试将响应中的 GraphQL 错误与返回的数据结合使用,以向用户尽可能地展示最佳内容,同时显示错误,以便他们知道为什么缺少某些部分。

结论

身份验证和授权是 API 设计中非常重要的主题,但正如本文所示,一旦我们了解了 HTTP API 身份验证的基础知识,我们就可以在 REST 和 GraphQL 上重复使用我们的技能,而无需进行太多改变。

了解授权是业务逻辑,并且通常完全针对特定软件产品进行定制,这一点也很重要。这意味着它应该被推到我们代码库的边缘,这样它就很容易被找到,并且不会在许多地方重复。

文章来源:Steps to building Authentication and Authorization for GraphQL APIs

#你可能也喜欢这些API文章!