Защита ваших API - это такой же важный компонент, как и все остальное в вашем приложении. Мы считаем, что GraphQL - это будущее API-интерфейсов, поэтому давайте обсудим, как их защитить. Прежде чем мы начнем, позвольте дать несколько определений. Этот пост про аутентификацию и авторизацию. Разница между ними только в нескольких буквах и в том, что они выполняют две разные задачи.

Аутентификация связана с входом пользователей в систему. Обычно для этого используется файл cookie, если вы строго создаете веб-страницу, или заголовок, если вы ориентируетесь на API. JWT становятся все более популярным способом аутентификации пользователей с помощью API.

Авторизация, хотя и похожа на аутентификацию, предполагает предоставление пользователям доступа к определенным ресурсам в API. Например, вы можете войти в систему и пройти аутентификацию как John Deer, но John Deer может не иметь доступа для обновления профиля Jane Doe и, следовательно, не авторизован для этой операции.

Часто в приложении есть такой рабочий процесс:

  1. Пользователь входит в систему, предоставляя имя пользователя и пароль, а взамен получает токен аутентификации.
  2. Клиентское приложение прикрепляет этот токен аутентификации к каждому будущему запросу (через заголовок авторизации).
  3. Каждый раз, когда сервер получает запрос, сервер проверяет токен и выбирает пользователя из базы данных.
  4. Выбранный пользовательский объект присоединяется к некоторому контексту, который течет по всему приложению.
  5. Различные части вашего приложения используют пользователя контекста, чтобы определить, авторизован ли пользователь для конкретной операции.
  6. Повторить.

Scaphold предоставляет ряд поставщиков аутентификации, таких как Auth0 для авторизации через социальные сети, Digits для аутентификации без пароля и аутентификации по традиционному имени пользователя и паролю.

Мы также предоставляем мощные механизмы авторизации с помощью разрешений. Scaphold поддерживает как реляционный, так и реляционный контроль доступа, который может быть многоуровневым для создания мощных систем авторизации.

Давайте по очереди рассмотрим каждую из этих концепций, чтобы узнать, как они помогают нам создавать более надежные API.

Аутентификация

Мы только что узнали, что аутентификация - это прикрепление запроса или сеанса к пользователю в базе данных. Если вы строите с использованием GraphQL API, скорее всего, вы будете использовать токены аутентификации. Мы предпочитаем JWT, поэтому давайте взглянем на один из них.

Анатомия JWT (веб-токен JSON)

Это пример JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRG9lIiwiYWRtaW4iOnRJgEdA

Это может выглядеть как случайный набор битов, но в хаосе есть порядок. Если вы перейдете на jwt.io, вы найдете отличный отладчик, который поможет вам разобраться в этом токене.

JWT состоит из трех частей, разделенных точкой ..

  1. Заголовок содержит алгоритм и тип токена.
  2. Полезная нагрузка имеет идентификатор пользователя sub, а также другие метаданные.
  3. Подпись - это контрольная сумма, которая вычисляется путем хеширования заголовка, полезной нагрузки и секретного ключа.

Когда вы декодируете указанный выше токен, вы должны увидеть эти значения

// header
{
  "alg": "HS256",
  "typ": "JWT"
}
// payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
// signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  <secret_key>
)

Аутентификация с REST

Итак, как наше приложение может использовать JWT? Если вы заметили в полезной нагрузке, есть поле с именем sub. sub обозначает тему и содержит идентификатор пользователя, которого представляет токен. Для аутентификации пользователя на сервере часто есть фрагмент кода, который служит привратником и проверяет эти токены. Если вы знакомы с javascript и выражаете, это может выглядеть примерно так.

import jwt from 'express-jwt';
import express from 'express';
import app = express();
app.get('/protected',
  jwt({secret: 'shhhhhhared-secret'}),
  function(req, res) {
    if (!req.user.admin) return res.sendStatus(401);
    res.sendStatus(200);
  });

Пакет express-jwt понимает, как декодировать JWT и проверять его подпись. Мы добавляем часть промежуточного программного обеспечения jwt({secret: 'shhhhhhared-secret'}) в маршрут /protected в нашем API, чтобы вы могли попасть в конечную точку /protected, только если вы прошли аутентификацию.

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

Аутентификация с GraphQL

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

Тот же самый серверный файл, показанный выше, может выглядеть так в GraphQL.

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 мутацию, чтобы получить токен для аутентификации. Это нехорошо.

Хорошая новость в том, что GraphQL предоставляет нам целый набор инструментов для создания еще более надежных механизмов аутентификации, чем мы могли бы с REST. Сначала мы рассмотрим основы, а затем я представлю метод, который сделает аутентификацию более масштабируемой и интересной в использовании.

Аутентифицируйте преобразователи, а не конечные точки.

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

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 вместо выдачи ошибки, и я бы действительно рекомендовал это.

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

Резольверы как совокупность функций

Если вы знакомы с express (или многими другими веб-фреймворками в этом отношении), вы, вероятно, слышали о функциях промежуточного программного обеспечения или использовали их. Фактически, когда мы ранее вызывали app.get('/protected', ...), мы применяли часть промежуточного программного обеспечения к /protected обработчику маршрута. Возможность применять подобное промежуточное ПО - очень мощный метод, который является примером более общей концепции программирования, называемой композиция функций. Это причудливый термин для объединения нескольких небольших функций для создания одной, более мощной.

Подобно тому, как express использует промежуточное ПО для обработки входящих и исходящих запросов, мы можем использовать композицию функций, чтобы сделать более удобные в обслуживании и СУХИЕ преобразователи GraphQL. Это тот же метод, который используется в наших логических функциях в scaphold, что позволяет вам переносить микросервисы со всего Интернета в ваш Scaphold GraphQL API.

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

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. Композиция функций - отличное решение для задач аутентификации, но вы также можете использовать ее для регистрации преобразователей, очистки ввода, обработки вывода и многого другого.

Авторизация в Скапхолде

Эти методы являются мощными строительными блоками для тех из вас, кто хочет создавать свои собственные серверы GraphQL. Если вы хотите начать работу прямо сейчас, то мы в Scaphold уже создали все это для вас, чтобы вы могли начать создавать отличные приложения. В Scaphold наши разрешения позволяют вам определять собственные правила контроля доступа к вашим данным. Существует 4 области разрешений, каждая из которых обеспечивает немного разное поведение.

КАЖДЫЙ Сфера

Прицел «Все» делает то, о чем вы могли догадаться. Он уполномочивает всех, независимо от того, аутентифицированы вы или нет. Это полезно, если вы хотите, чтобы любой пользователь мог, например, читать общедоступный форум.

Аутентифицированный объем

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

РОЛЬ Сфера

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

Сначала создайте список и зарегистрируйте пользователя в роли.

# First create a role
mutation CreateRole($newrole:CreateRoleInput!) {
  createRole(input:$newrole) {
    changedRole {
      id
      name
    }
  }
}
# Then enroll a user into that role
mutation EnrollUser($enrollment:AddToUserRolesConnectionInput!) {
  addToUserRolesConnection(input:$enrollment) {
    changedUserRoles {
      user {
        id
      }
      role {
        id
      }
    }
  }
}

Затем добавьте разрешение области ROLE для определенных операций для типа с помощью кнопки Добавить разрешение в конструкторе схемы.

ВЗАИМОСВЯЗЬ

Область отношения - это наиболее продвинутая область разрешений. Разрешения на основе ролей позволяют вручную управлять членством в ролях и предоставлять пользователям доступ сразу ко всем типам. Область отношения использует естественные соединения в вашем API, чтобы предоставить пользователям доступ к объектам, к которым они подключены через некоторый путь к пользовательскому полю. Давайте посмотрим на пример из такого приложения, как Slack.

# Our simple slack schema
type User {
  id: ID!
  channels: ChannelConnection
  messages: MessageConnection
}
type Channel {
  id: ID!
  name: String!
  members: UserConnection
  messages: MessageConnection
}
type Message {
  id: ID!
  content: String!
  channel: Channel
  author: User
}

Допустим, мы создавали слабину и хотели, чтобы наши пользователи могли читать и создавать сообщения только в каналах, участниками которых они являются. Для этого мы создадим следующее разрешение с областью действия RELATION

Как только это разрешение получено, вы можете запрашивать объекты, связанные с вашим пользователем через viewer.user. Мы используем систему типов graphql, чтобы обеспечить высокую производительность, поскольку нам не нужно отправлять какие-либо другие запросы, чтобы знать, что вы привязаны к результатам, когда вы запрашиваете через viewer.user.

query MyChannelsAndMessages {
  viewer {
    user {
      channels {
        edges {
          node {
            name
            messages {
              edges {
                node {
                  id
                  content
                }
              }
            }
          }
        }
      }
    }
  }
}

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

mutation CreateMessage($input:CreateMessageInput!) {
  createMessage(input:$input) {
    changedMessage {
      id
      content
      channel {
        name
      }
    }
  }
}
# Variables
{
  "input": {
    "content": "I rest easy when my data is protected.",
    "channelId": "ABC" # This value determines if the operation will succeed
  }
}

Разрешения с областью действия RELATION могут иметь произвольно длинные пути к пользовательским полям. Это означает, что вы можете накладывать разрешения на любую операцию, которая связана с пользователем через некоторый путь в вашем графике API. Следите за обновлениями, потому что у нас есть еще более мощные функции разрешений, которые дадут вам еще больше свободы в принятии решения о том, как вы хотите заблокировать свои данные.

Надеюсь, этот пост помог лучше понять, как можно реализовать аутентификацию с помощью GraphQL. Если вам не терпится приступить к созданию с помощью GraphQL, попробуйте платформу Scaphold’s GraphQL Backend as a Service. Мы уже создали все это и многое другое, чтобы вы могли получить масштабируемый и безопасный API GraphQL, который можно расширить с помощью собственной бизнес-логики для быстрого создания приложений. К тому же это бесплатно для разработки!

Спасибо за прочтение! Пожалуйста, дайте мне знать, что вы думаете, в комментариях!

Удачного Scapholding!

Присоединяйтесь к Scaphold на слабину

Первоначально опубликовано на scaphold.io.