身份验证和授权
控制对您的 GraphQL API 的访问
您的GraphQL API 可能需要控制哪些用户可以查看和交互各种提供的数据。
- 身份验证 是确定给定的用户是否已登录,进而确定 某人是哪个用户。。
- 授权 然后确定给定的用户可以做什么或查看什么。
💡 小贴士
GraphOS 路由器 现在可以为您的整个 supergraph 提供身份验证和授权。虽然在不同 subgraph 或单体 graph 层面上重新应用身份验证检查可能是合理的,但 GraphOS 路由器已经构建并提供了标准的 JWT 检查,可以通过简单的 YAML 配置设置,并为中心位置的所有 subgraph 强制执行:
https://apollo.graphql.net.cn/blog/centrally-enforce-policy-as-code-for-graphql-apis
将已验证用户信息放入您的上下文值
在我们能正确控制数据访问之前,我们必须验证用户身份。提供身份验证凭据有许多模式,包括HTTP头和JSON网络令牌。
以下示例从每个Authorization
请求头操作中提取用户令牌。然后它获取该令牌对应的中国用户对象,并将其添加到传递给每个正在执行解析器的contextValue对象中。每个解析器都可以使用此对象来确定用户可以访问哪些数据。
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';interface MyContext {// we'd define the properties a user should have// in a separate user interface (e.g., email, id, url, etc.)user: UserInterface;}const server = new ApolloServer<MyContext>({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {// Note: This example uses the `req` argument to access headers,// but the arguments received by `context` vary by integration.// This means they vary for Express, Fastify, Lambda, etc.// For `startStandaloneServer`, the `req` and `res` objects are// `http.IncomingMessage` and `http.ServerResponse` types.context: async ({ req, res }) => {// Get the user token from the headers.const token = req.headers.authorization || '';// Try to retrieve a user with the tokenconst user = await getUser(token);// Add the user to the contextreturn { user };},});console.log(`🚀 Server listening at: ${url}`);
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';const server = new ApolloServer({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {// Note: This example uses the `req` argument to access headers,// but the arguments received by `context` vary by integration.// This means they vary for Express, Fastify, Lambda, etc.// For `startStandaloneServer`, the `req` and `res` objects are// `http.IncomingMessage` and `http.ServerResponse` types.context: async ({ req, res }) => {// Get the user token from the headers.const token = req.headers.authorization || '';// Try to retrieve a user with the tokenconst user = await getUser(token);// Add the user to the contextreturn { user };},});console.log(`🚀 Server listening at: ${url}`);
由于您的contextValue
是针对每个新的请求独立生成的,所以我们不必担心清理操作执行完成后的用户数据。
针对不同身份验证方式获取用户的具体操作会有所不同,但最终步骤基本上是相似的。您的模式可能需要您将{ loggedIn: true }
放入contextValue
,但是可能也需要一个id或角色,比如{ user: { id: 12345, roles: ['user', 'admin'] } }
。
在接下来的章节中,我们将探讨如何利用我们现有的用户信息来保护您的模式。
授权方法
API全局授权
一旦我们有了有关发起请求的用户的信息,最基本的事情是我们可以完全禁止他们根据其角色执行查询的能力。我们将从这个全有或全无的授权方法开始,因为它是最基本的。
我们应仅在高度限制的环境中使用此方法,这些环境完全不允许公共访问API,例如内部工具或不应面向公众的独立微服务。
为了执行此类授权,我们可以在context
函数中添加生成GraphQLError
的抛出,如果用户未认证:
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { GraphQLError } from 'graphql';interface MyContext {user: UserInterface;}const server = new ApolloServer<MyContext>({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {// get the user token from the headersconst token = req.headers.authorization || '';// try to retrieve a user with the tokenconst user = getUser(token);// optionally block the user// we could also check user roles/permissions hereif (!user)// throwing a `GraphQLError` here allows us to specify an HTTP status code,// standard `Error`s will have a 500 status code by defaultthrow new GraphQLError('User is not authenticated', {extensions: {code: 'UNAUTHENTICATED',http: { status: 401 },},});// add the user to the contextreturn { user };},});console.log(`🚀 Server listening at: ${url}`);
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { GraphQLError } from 'graphql';const server = new ApolloServer({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async ({ req }) => {// get the user token from the headersconst token = req.headers.authorization || '';// try to retrieve a user with the tokenconst user = getUser(token);// optionally block the user// we could also check user roles/permissions hereif (!user)// throwing a `GraphQLError` here allows us to specify an HTTP status code,// standard `Error`s will have a 500 status code by defaultthrow new GraphQLError('User is not authenticated', {extensions: {code: 'UNAUTHENTICATED',http: { status: 401 },},});// add the user to the contextreturn { user };},});console.log(`🚀 Server listening at: ${url}`);
与基本上下文函数唯一的区别在于对用户的检查。如果不存在用户或查找失败,该函数将抛出错误,相应的operation将不会执行。
在解析器中
虽然API全局授权在某些情况下可能很有用,但更常见的是,一个GraphQL API至少包含几个公开的字段。例如,新闻网站可能向任何人都展示文章预览,但限制完整文章只在付费客户中展示。
幸运的是,GraphQL提供了对数据的非常细粒度控制。在GraphQL服务器中,单个字段解析器可以检查用户角色,并为每个用户决定返回什么内容。在前面的章节中,我们看到了如何将用户信息附加到contextValue
对象上。在本文的其余部分,我们将讨论如何使用该对象。
以我们的第一个示例为例,让我们看看一个仅对有效用户可访问的resolver:
users: (parent, args, contextValue) => {// In this case, we'll pretend there is no data when// we're not logged in. Another option would be to// throw an error.if (!contextValue.user) return null;return ['bob', 'jake'];};
这个例子是我们在schema中命名的名为users
的字段,它返回用户名单。该函数第一行上的if检查查看我们请求生成的contextValue,检查是否存在user对象,如果不存在,则该field的整个返回null。
当我们构建解析器时,需要做出的一个选择是未授权字段应该如何返回值。在一些使用场景中,在这里返回null
是完全有效的。替代方案包括返回一个空数组[]
或者抛出一个错误,告知客户端他们无权访问该字段。为了简化,在这个示例中我们只是返回了null
。
现在让我们进一步扩展这个例子,并仅允许具有admin
角色的用户查看我们的用户列表。毕竟,我们可能不希望任何人都能访问所有用户。
users: (parent, args, contextValue) => {if (!contextValue.user || !contextValue.user.roles.includes('admin')) return null;return contextValue.models.User.getAll();};
这个例子几乎和上一个例子相同,只是增加了一个条件:它期望用户对象上的roles
数组包含admin
角色。否则,它返回null
。这样进行授权的好处是我们可以跳过我们的解析器,并且当没有权限使用时不需要调用查找函数,从而限制可能暴露敏感数据的问题。
由于我们的解析器可以访问contextValue
中的所有内容,一个重要的问题是我们要在该对象中保留多少信息。例如,我们不需要用户的id、姓名或年龄(至少目前不需要)。最好的做法是保留这些信息,直到它们被需要,因为它们很容易在后面添加回来。
在数据模型中
随着服务器变得越来越复杂,模式中可能需要在不同地方获取相同类型的数据。在上一个示例中,你可能已经注意到返回数组被替换为了对contextValue.models.User.getAll()
的调用。
和以往一样,我们建议将实际的数据获取和转换逻辑从您的解析器移动到数据源或模型对象,每个对象代表应用程序中的一个概念:User
、Post
等。这允许您将解析器作为薄路由层,所有业务逻辑都在一个地方。
例如,一个用于User
的模型文件可能包含所有操作用户的逻辑,可能看起来像这样:
export const User = {getAll: () => {/* fetching/transformation logic for all users */},getById: (id) => {/* fetching/transformation logic for a single user */},getByGroupId: (id) => {/* fetching/transformation logic for a group of users */},};
在以下示例中,我们的模式有多种方式请求单个用户
type Query {user(id: ID!): Userarticle(id: ID!): Article}type Article {author: User}type User {id: ID!name: String!}
与其在两个不同的地方使用相同的抓取逻辑来处理单个用户,通常将此逻辑移动到模型文件会更合理。你可能已经猜到了,既然在一个授权文章中一直在谈论模型文件,那么授权也是可以委托给模型,就像数据抓取一样。你是对的。
将授权委托给模型
你可能已经注意到,我们的模型也存在于contextValue
,除了我们之前添加的用户对象。我们可以以完全相同的方式将模型添加到上下文中,就像添加用户一样。
context: async ({ req }) => {// get the user token from the headersconst token = req.headers.authentication || '';// try to retrieve a user with the tokenconst user = getUser(token);// optionally block the user// we could also check user roles/permissions hereif (!user) throw new GraphQLError("you must be logged in to query this schema", {extensions: {code: 'UNAUTHENTICATED',},});// add the user to the contextValuereturn {user,models: {User: generateUserModel({ user }),...}};},
用函数生成我们的模型需要一些小的重构,这样我们的User模型看起来可能就是这样的
export const generateUserModel = ({ user }) => ({getAll: () => {/* fetching/transform logic for all users */},getById: (id) => {/* fetching/transform logic for a single user */},getByGroupId: (id) => {/* fetching/transform logic for a group of users */},});
现在,User
中的任何模型方法都可以访问到 resolver 已经有的相同的 user
信息,这允许我们将 getAll
函数重构为直接执行权限检查,而不是将其放在 resolver:
getAll: () => {if (!user || !user.roles.includes('admin')) return null;return fetch('http://myurl.com/users');};
使用自定义指令
授权的另一种方式是通过GraphQL Schema 指令。指令是一个以 @ 符号开头的标识符,后面可以跟着一系列命名的参数,它几乎可以出现在 GraphQL 查询或模式语言中的任何形式的语法之后。
以下是授权指令的一个示例:
const typeDefs = `#graphqldirective @auth(requires: Role = ADMIN) on OBJECT | FIELD_DEFINITIONenum Role {ADMINREVIEWERUSER}type User @auth(requires: USER) {name: Stringbanned: Boolean @auth(requires: ADMIN)canPost: Boolean @auth(requires: REVIEWER)}`;
可以像这样直接在特定的字段或类型上调用 @auth
指令。这巧妙地将授权逻辑隐藏在指令实现后面。
有了你的 @auth
指令,你的 resolver 现在需要检查用户的角色与指令中指定的角色是否匹配。一种方法是通过使用mapSchema
函数从@graphql-tools/utils
包中的函数来转换你的模式中的每个 resolver。有关设置基于 directive 的权限检查的示例,请参阅mapSchema
文档。
在 GraphQL 之外
如果你使用的是一个具有内置授权的 REST API,比如使用 HTTP 标头,你还有一个选择。你不需要在 GraphQL 层(在 resolver/models 中)进行任何身份验证或授权工作,只需简单地将标头或 cookie 传递给你的 REST 端点,让其完成工作即可。
以下是一个示例
context: async ({ req }) => {// pass the request information through to the modelreturn {user,models: {User: generateUserModel({ req }),...}};},
export const generateUserModel = ({ req }) => ({getAll: () => {return fetch('http://myurl.com/users', { token: req.headers.token });},});
如果你的REST接口已经通过某种形式的授权进行保护,这将在GraphQL层中减少很多需要构建的逻辑。当在已具备所需一切功能的现有REST API上构建GraphQL API时,这可以是一个非常不错的选择。