Graphql @include with expression

Ana*_*and 10 authentication permissions authorization graphql

我正在实现一个查询,该查询应根据用户登录状态为响应中的某些字段提供服务.

具体来说,我希望只有传递"pointRate"字段$authenticationToken并希望避免传入$authenticated下面的查询.我想避免发送的原因$authenticated是客户端可以通过发送$authenticated = true而做错$authenticationToken = null.

query ToDoQuery($authenticationToken: String, $authenticated: Boolean!) {
    pointRate(accessToken: $authenticationToken) @include(if: $authenticated) {
        status
    }
}
Run Code Online (Sandbox Code Playgroud)

Sky*_*ker 6

所以,其实你想这样做

i)如果传递了$ authenticationToken,你想获得"pointRate".

ii)并且您还希望避免在后续查询中传递$ authenticated.因为您担心您的客户可能会犯一些错误,例如在身份验证令牌为空的情况下发送经过身份验证的错误.

所以一般来说我想回答一下,如果你想使用GraphQL自己处理身份验证,首先你必须创建一个令牌,然后你必须在每个请求或后续请求中传递令牌.否则就不可能.因为没有身份验证就不会提供敏感数据.

另一方面,您可以使用会话身份验证.您可以访问每个数据,直到会话关闭.

如果不满意,您可以使用像您这样的场景阅读以下简要说明.我还试图积累一些相关的样本解决方案以便更好地理解,它可能会更多地澄清你.

由于GraphQL API完全公开,您可以通过两种方式进行身份验证.

  1. 让Web服务器(例如express或nginx)负责身份验证.
  2. 在GraphQL中处理身份验证.

如果您在Web服务器中进行身份验证,则可以使用标准身份验证程序包(例如,express.js for express),许多现有的身份验证方法都可以直接使用.您也可以根据自己的喜好添加和删除方法,而无需修改GraphQL架构.

如果您自己实施身份验证,请执行以下操作

  • 确保永远不要以明文或MD5或SHA-256哈希存储密码

    • 使用像bcrypt这样的东西

    • 确保不在服务器上按原样存储会话令牌,您应该先将它们哈希

    • 您可以编写一个登录方法,用于设置上下文.由于突变是一个接一个地执行而不是并行执行,因此可以确保在登录突变之后设置了上下文:

      mutation { loginWithToken(token: "6e37a03e-9ee4-42fd-912d-3f67d2d0d852"), do_stuff(greeting: "Hello", name: "Tom"), do_more_stuff(submarine_color: "Yellow") }

    • 我们不是通过标头或查询参数(如JWT,OAuth等)传递令牌,而是将其作为GraphQL查询的一部分.您的架构代码可以使用JWT库本身或其他工具直接解析令牌.

    • 记得在传递敏感信息时始终使用HTTPS :)

因为并行执行对性能很重要.并且按照给定的顺序连续执行变异和查询.因此,在大多数情况下,最好在Web服务器中处理身份验证.它不仅更通用,而且更灵活.


之情况:

首先要做到以下几点

import jwt from'express-jwt';
import graphqlHTTP from'express-graphql';
import express from'express';
import schema from'./mySchema';
const app = express();

app.use('/graphql', jwt({
  secret: 'shhhhhhared-secret',
  requestProperty: 'auth',
  credentialsRequired: false,
}));
app.use('/graphql', function(req, res, done) {
  const user = db.User.get(req.auth.sub);
  req.context = {
    user: user,
  }
  done();
});
app.use('/graphql', graphqlHTTP(req => ({
    schema: schema,
    context: req.context,
  })
));
Run Code Online (Sandbox Code Playgroud)

如果您签入上述部分,您将获得该API根本不安全.它可能会尝试验证JWT,但如果JWT不存在或无效,请求仍将通过(请参阅credentialsRequired:false).为什么?我们必须允许请求通过,因为如果我们阻止它,我们将阻止整个API.这意味着,我们的用户甚至无法调用loginUser变异来获取令牌以进行身份​​验证.


解决方案1:

使用Authenticate解析器而不是端点的Barebone示例.

import { GraphQLSchema } from 'graphql';
import { Registry } from 'graphql-helpers';

// The registry wraps graphql-js and is more concise
const registry = new Registry();

registry.createType(`
  type User {
    id: ID!
    username: String!
  }
`;

registry.createType(`
  type Query {
    me: User
  }
`, {
  me: (parent, args, context, info) => {
    if (context.user) {
      return context.user;
    }
    throw new Error('User is not logged in (or authenticated).');
  },
};

const schema = new GraphQLSchema({
  query: registry.getType('Query'),
});
Run Code Online (Sandbox Code Playgroud)

当请求到达Query.me解析器时,服务器中间件已经尝试对用户进行身份验证并从数据库中获取用户对象.在我们的解析器中,我们可以检查用户的graphql上下文(我们在server.js文件中设置上下文),如果存在,则返回它,否则抛出错误.

注意:您可以轻松返回null而不是抛出错误,我实际上会推荐它.

解决方案2:

使用express-graphql的功能组合(基于中间件)

import { GraphQLSchema } from 'graphql';
import { Registry } from 'graphql-helpers';
// See an implementation of compose https://gist.github.com/mlp5ab/f5cdee0fe7d5ed4e6a2be348b81eac12
import { compose } from './compose';

const registry = new Registry();

/**
* The authenticated function checks for a user and calls the next function in the composition if
* one exists. If no user exists in the context then an error is thrown.
*/
const authenticated =
  (fn: GraphQLFieldResolver) =>
  (parent, args, context, info) => {
    if (context.user) {
      return fn(parent, args, context, info);
    }
    throw new Error('User is not authenticated');
  };

/*
* getLoggedInUser returns the logged in user from the context.
*/
const getLoggedInUser = (parent, args, context, info) => context.user;

registry.createType(`
  type User {
    id: ID!
    username: String!
  }
`;

registry.createType(`
  type Query {
    me: User
  }
`, {
  me: compose(authenticated)(getLoggedInUser)
};

const schema = new GraphQLSchema({
  query: registry.getType('Query'),
});
Run Code Online (Sandbox Code Playgroud)

上面的代码与第一个代码段完全相同.我们创建了一个高度可重用且可测试的中间件功能,而不是在我们的主解析器功能中检查用户,而是实现了同样的功能.这种设计的直接影响可能还不明显,但想想如果我们想要添加另一条受保护路线以及记录我们的解析器运行时间会发生什么.我们的新设计简单如下:

const traceResolve =
  (fn: GraphQLFieldResolver) =>
  async (obj: any, args: any, context: any, info: any) => {
    const start = new Date().getTime();
    const result = await fn(obj, args, context, info);
    const end = new Date().getTime();
    console.log(`Resolver took ${end - start} ms`);
    return result;
  };

registry.createType(`
  type Query {
    me: User
    otherSecretData: SecretData
  }
`, {
  me: compose(traceResolve, authenticated)(getLoggedInUser)
  otherSecretData: compose(traceResolve, authenticated)(getSecretData)
};
Run Code Online (Sandbox Code Playgroud)

使用此技术将帮助您构建更强大的GraphQL API.函数组合是身份验证任务的绝佳解决方案,但您也可以使用它来记录解析器,清理输入,按摩输出等等.

解决方案3:

一个不错的解决方案是将数据提取分解到单独的层中并在那里进行授权检查.以下是遵循上述原则的示例.它是用于获取用户可以看到的所有待办事项列表的查询.

对于以下查询,

{
  allLists {
    name
  }
}
Run Code Online (Sandbox Code Playgroud)

不要这样做:

//in schema.js (just the essential bits)
allLists: {
  resolve: (root, _, ctx) => {
    return sql.raw("SELECT * FROM lists WHERE owner_id is NULL or owner_id = %s", ctx.user_id);
  }
}
Run Code Online (Sandbox Code Playgroud)

相反,我建议你这样做:

// in schema.js (just the essential bits)
allLists: {
  resolve: (root, _, ctx) => {
    //factor out data fetching
    return DB.Lists.all(ctx.user_id)
      .then( lists => {
        //enforce auth on each node
        return lists.map(auth.List.enforce_read_perm(ctx.user_id) );
      });
  }
}
//in DB.js 
export const DB = {
  Lists: {
    all: (user_id) => {
      return sql.raw("SELECT id FROM lists WHERE owner_id is NULL or owner_id = %s, user_id);
    }
  }
}
//in auth.js
export const auth = {
  List: {
   enforce_read_perm: (user_id) => {
     return (list) => {
       if(list.owner_id !== null && list.owner_id !== user_id){
         throw new Error("User not authorized to read list");
       } else {
         return list;
       }
     }
   }
}
Run Code Online (Sandbox Code Playgroud)

您可能认为DB.Lists.all函数已经强制执行权限,但我认为它只是尝试不获取太多数据,而权限本身并不是单独强制执行每个节点.这样,即使您在许多不同的地方获取数据,您也可以在一个地方进行身份验证,并确保它们能够一致地应用.

解决方法4:

可以以许多不同方式完成验证流程.

i) basic auth, 
ii) session auth, or
iii) token auth.
Run Code Online (Sandbox Code Playgroud)

由于您的问题是根据令牌身份验证,我想与Scaphold见面,哪一个使用令牌身份验证.我们所做的一切,无论是将用户登录到Scaphold还是将用户登录到您的应用程序,我们都使用令牌来管理用户的身份验证状态.auth流程的工作方式如下:

a)用户使用用户名和密码登录.

b)GraphQL服务器根据他/她的散列密码验证数据库中的用户.

c)如果成功,服务器返回JSON Web令牌(JWT),它是具有到期日期的Base64编码令牌.这是身份验证令牌.

d)要使用身份验证令牌,您的未来请求应在头中包含身份验证令牌

{授权:'Bearer'+ [Auth_Token]}

现在,每次服务器(可能是Node Express)在标头中看到令牌时,它都会解析令牌,验证它,并在GraphQL世界中,将已识别的用户保存在上下文中,以便在应用程序的其余部分中使用.用户现在已登录.

有关更多信息,您可以在本教程中了解有关@include的更多信息:https://github.com/mugli/learning-graphql/blob/master/4.%20Querying%20with%20Directives.md#include

要逐步学习graphql身份验证,您可以阅读本教程:GraphQL身份验证

资源链接:

  1. 使用GraphQL进行身份验证
  2. GraphQL中的身份验证指南
  3. GraphQL安全性的最佳实践


zwi*_*pie 1

我认为这是不可能的,因为你无法在 GraphQL 中将(空)字符串转换为布尔值。

另外,官方 GraphQL 文档的一些建议:

将授权逻辑委托给业务逻辑层