使用中继进行身份验证和访问控制

Authentication and Access Control with Relay

Facebook 的官方说法是 Relay 是“intentionally agnostic about authentication mechanisms”。在 Relay 存储库中的所有示例中,身份验证和访问控制是一个单独的问题。在实践中,我还没有找到一种简单的方法来实现这种分离。

Relay 存储库中提供的示例都具有根架构,其中包含一个假定只有一个用户的 viewer 字段。该用户可以访问所有内容。

然而,在现实中,一个应用程序有很多用户,每个用户对每个节点都有不同程度的访问权限。

假设我在 JavaScript 中有这个模式:

export const Schema = new GraphQLSchema({
    query: new GraphQLObjectType({
        name: 'Query',
        fields: () => ({
            node: nodeField,
            user: {
                type: new GraphQLObjectType({
                    name: 'User',
                    args: {
                        // The `id` of the user being queried for
                        id: { type: new GraphQLNonNull(GraphQLID) },
                        // Identity the user who is querying
                        session: { type: new GraphQLInputObjectType({ ... }) },
                    },
                    resolve: (_, { id, session }) => {
                        // Given `session, get user with `id`
                        return data.getUser({ id, session });
                    }
                    fields: () => ({
                        name: {
                            type: GraphQLString,
                            resolve: user => {
                                // Does `session` have access to this user's
                                // name?
                                user.name
                            }
                        }
                    })
                })
            }
        })
    })
});

从查询用户的角度来看,有些用户是完全私密的。其他用户可能只向查询用户公开某些字段。所以要获取用户,客户端不仅要提供他们查询的用户ID,还必须标识自己,这样才能进行访问控制。

这似乎很快就会变得复杂,因为控制访问的需要会沿着图表向下延伸。

此外,我需要控制每个根查询的访问权限,例如 nodeField。我需要确保每个节点都实现 nodeInterface.

所有这些看起来都是很多重复性的工作。是否有任何已知的模式来简化这个?我是不是想错了?

不同的应用程序对访问控制的形式有非常不同的要求,因此将某些东西烘焙到基本的 Relay 框架或 GraphQL 参考实现中可能没有意义。

我看到的一种非常成功的方法是将 privacy/access 控件烘焙到数据 model/data 加载程序框架中。每次加载对象时,您不仅会通过 id 加载它,还会提供查看器的上下文。如果查看器看不到该对象,它将无法加载 就好像它不存在一样 以防止甚至泄露该对象的存在。该对象还保留查看器上下文,并且某些字段可能具有限制访问权限,这些访问权限在从对象 returned 之前进行检查。在较低级别的数据加载机制中烘焙它有助于确保较高级别产品/GraphQL 代码中的错误不会泄漏私有数据。

举个具体的例子,我可能不能看到某个用户,因为他拉黑了我。你可能会被允许看到他的一般情况,但不能看到他的电子邮件,因为你和他不是朋友。

在这样的代码中:

var viewer = new Viewer(getLoggedInUser());
User.load(id, viewer).then(
  (user) => console.log("User name:", user.name),
  (error) => console.log("User does not exist or you don't have access.")
)

尝试在 GraphQL 级别实现可见性很可能会泄露信息。想一想在 Facebook 的 GraphQL 实现中访问用户的多种方式:

node($userID) { name }
node($postID) { author { name } }
node($postID) { likers { name } }
node($otherUserID) { friends { name } }

所有这些查询都可以加载用户名,如果用户阻止了您,none 其中应该 return 用户或其名称。拥有对所有这些字段的访问控制并且不要忘记任何地方的检查是错过检查某处.

的一个秘诀

我发现如果使用 GraphQL rootValue,处理身份验证很容易,当针对模式执行查询时,它会传递给执行引擎。该值在所有执行级别都可用,可用于存储访问令牌或任何标识当前用户的内容。

如果您使用 express-graphql 中间件,您可以在 GraphQL 中间件之前的中间件中加载会话,然后配置 GraphQL 中间件以将该会话放入根值中:

function getSession(req, res, next) {
  loadSession(req).then(session => {
    req.session = session;
    next();
  }).catch(
    res.sendStatus(400);
  );
}

app.use('/graphql', getSession, graphqlHTTP(({ session }) => ({
  schema: schema,
  rootValue: { session }
})));

此会话随后可在架构中的任何深度使用:

new GraphQLObjectType({
  name: 'MyType',
  fields: {
    myField: {
      type: GraphQLString,
      resolve(parentValue, _, { rootValue: { session } }) {
        // use `session` here
      }
    }
  }
});

您可以将此与 "viewer-oriented" 数据加载配对以实现访问控制。查看 https://github.com/facebook/dataloader,它有助于创建此类数据加载对象并提供批处理和缓存。

function createLoaders(authToken) {
  return {
    users: new DataLoader(ids => genUsers(authToken, ids)),
    cdnUrls: new DataLoader(rawUrls => genCdnUrls(authToken, rawUrls)),
    stories: new DataLoader(keys => genStories(authToken, keys)),
  };
}

如果有人对此主题有疑问:我根据 dimadima 的回答为 Relay/GraphQL/express 身份验证做了一个示例 repo。它使用 express 中间件和 GraphQL Mutation

将会话数据(userId 和角色)保存在 cookie 中