Graphql @include с выражением

Я выполняю запрос, который должен обслуживать некоторые поля в ответе в зависимости от состояния входа пользователя.

Чтобы быть конкретным, я хочу получить поле "pointRate" только в том случае, если $authenticationToken передано и будет хотеть избежать передачи $authenticated в нижнем запросе. Причина, по которой я хочу избежать отправки $authenticated, заключается в том, что клиент может ошибиться, отправив $authenticated = true, но $authenticationToken = null.

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

Ответ 1

Итак, на самом деле вы хотите сделать это

i), если передано $authenticationToken, вы хотите получить "pointRate".

ii), и вы также хотите избежать передачи $authenticated в последующих запросы. Поскольку вы беспокоитесь о своих клиентах, которые могут ошибка, аналогичная отправке аутентифицированной, верна, когда токен аутентификации было null.

Поэтому в целом я хочу ответить, что если вы хотите самостоятельно обрабатывать аутентификацию с помощью GraphQL, сначала вам нужно создать токен, тогда вам нужно передать токен в каждом запросе или с последующими запросами. В противном случае это невозможно. Потому что конфиденциальные данные не будут предоставляться без проверки подлинности.

С другой стороны, вы можете использовать session auth. Вы можете получить доступ к каждой информации до закрытия сеанса.

Если это неудовлетворительно, вы можете прочитать следующее краткое описание с помощью scenerio, подобного вашему. Я также попытался собрать некоторые связанные с ними образцы решений для лучшего понимания, это может разъяснить вам больше.

Поскольку GraphQL API полностью открыт, вы можете сделать аутентификацию двумя способами.

  • Пусть веб-сервер (например, express или nginx) позаботится об аутентификации.
  • Обработать аутентификацию в самом GraphQL.

Если вы выполняете аутентификацию на веб-сервере, вы можете использовать стандартный пакет auth (например, passport.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 при передаче конфиденциальной информации:)

Поскольку параллельное выполнение является важным для производительности. а мутация и запросы выполняются серийно, в указанном порядке. Поэтому в большинстве случаев предпочтительно обрабатывать аутентификацию на веб-сервере. Его не только более общий, но и более гибкий.


Scenerio:

Сначала выполните следующие действия

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,
  })
));

Если вы зарегистрируетесь в этом разделе, вы получите тот API, который не является безопасным вообще. Он может попытаться проверить JWT, но если JWT не существует или недействителен, запрос все равно пройдет (см. CredentialsRequired: false). Зачем? Мы должны разрешить запрос проходить, потому что, если мы заблокировали его, мы заблокируем весь API. Это означает, что наши пользователи даже не смогут вызвать мутацию loginUser, чтобы получить токен для аутентификации.


Решение # 1:

Пример Barebone с использованием Authenticate resolvers, а не конечных точек.

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'),
});

К моменту поступления запроса на наш запрос 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'),
});

Вышеприведенный код будет работать точно так же, как и первый фрагмент. Вместо того, чтобы проверять пользователя в нашей главной функции распознавателя, мы создали многократно используемую и проверяемую промежуточную функцию, которая обеспечивает одно и то же. Непосредственное влияние этого проекта может быть пока не очевидным, но подумайте о том, что произойдет, если мы захотим добавить еще один защищенный маршрут, а также запишите время работы нашего резольвера. Благодаря нашему новому дизайну он прост как:

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)
};

Использование этого метода поможет вам создать более надежные API-интерфейсы GraphQL. Состав функций - отличное решение для задач аутентификации, но вы также можете использовать его для регистрации решателей, очистки ввода, вывода массивов и т.д.

Решение # 3:

Приличным решением является разложение данных на отдельный слой и проверка авторизации там. Ниже приведен пример, который следует принципам, изложенным выше. Его для запроса, который извлекает все списки задач, которые пользователь может видеть.

Для следующего запроса

{
  allLists {
    name
  }
}

Не делайте этого:

//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);
  }
}

Вместо этого я предлагаю вам сделать следующее:

// 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;
       }
     }
   }
}

Вы можете подумать, что функция DB.Lists.all уже применяет разрешения, но, как я вижу, она просто пытается не получать слишком много данных, сами разрешения выполняются не на каждом node отдельно. Таким образом, у вас есть проверки подлинности в одном месте и вы можете быть уверены, что они будут применяться последовательно, даже если вы извлекаете данные во многих разных местах.

Решение # 4:

Поток Auth может быть выполнен различными способами.

i) basic auth, 
ii) session auth, or
iii) token auth.

Как ваша проблема в соответствии с токеном auth, я хотел бы встретиться с вами в Scaphold, который использует аутентификацию по токенам. Все, что мы делаем, будь то регистрация пользователя в Scaphold или регистрация пользователя в вашем приложении, мы используем токены для управления статусом авторизации пользователя. Поток auth работает следующим образом:

a) Пользователь регистрируется с именем пользователя и паролем.

b) Сервер GraphQL проверяет пользователя в базе данных на свой хешированный пароль.

c) В случае успеха сервер возвращает JNON Web Token (JWT), который является маркером с кодировкой Base64 с датой истечения срока действия. Это токен аутентификации.

d) Чтобы использовать токен аутентификации, ваши будущие запросы должны включать токен аутентификации в заголовке в виде

{Авторизация: "Носитель" + [Авт_Токен]}

Теперь, каждый раз, когда сервер (возможно, node Express) видит маркер в заголовке, он будет разбирать токен, проверять его и в мире GraphQL, сохранять идентифицированного пользователя в контексте для использования в остальной части приложения. Пользователь теперь вошел в систему.

Более подробно вы можете узнать о @include в этом учебнике: https://github.com/mugli/learning-graphql/blob/master/4.%20Querying%20with%20Directives.md#include

Для обучения поэтапной аутентификации graphql вы можете пройти этот учебник: Аутентификация GraphQL

Ссылка ресурса:

Ответ 2

Я не думаю, что это возможно, поскольку вы не можете преобразовать (пустой) String в Boolean в GraphQL.

Кроме того, некоторые рекомендации официальных графических документов:

Делегирование логики авторизации на уровне бизнес-логики

Ответ 3

@include

Запросы GraphQL - это мощный способ объявления данных в вашем приложении. Директива include включает в себя поля, основанные на некоторых условиях.

query myAwesomeQuery($isAwesome: Boolean) {
  awesomeField @include(if: $isAwesome)
}

Примечание. @skip всегда имеет более высокий приоритет, чем @include.