Контекст и авторизация в резолверах

Объект context создаётся на каждый запрос и несёт текущего пользователя. Через него резолверы решают, кто что имеет право увидеть или изменить.

В GraphQL один эндпоинт — значит, привычная REST-защита «по URL» не работает. Права проверяются внутри, в резолверах, на уровне полей.

В REST доступ часто ограничивают по маршруту: «к /admin/* пускаем только админов». В GraphQL маршрут один, а полей сотни, поэтому авторизацию переносят внутрь — в резолверы, опираясь на context. Напомним: context создаётся функцией на каждый запрос и обычно содержит расшифрованного из токена пользователя. Эта разница принципиальна: в GraphQL нельзя «прикрыть» чувствительные данные тем, что они лежат на отдельном URL, — все данные доступны через один и тот же эндпоинт. Зато появляется и более тонкий контроль: права можно проверять не только на уровне целых операций, но и на уровне отдельных полей. Один и тот же тип User может отдавать публичное имя кому угодно, а email — только владельцу профиля или администратору. Такую гранулярность в REST имитировать заметно сложнее.

Аутентификация vs авторизация

  • Аутентификация — «кто ты?». Происходит один раз при создании context: разбираем токен из заголовка, находим пользователя.
  • Авторизация — «что тебе можно?». Происходит в резолверах: проверяем права на конкретное поле или действие.
// Аутентификация — в context (один раз на запрос)
context: async ({ req }) => {
  const user = await userFromToken(req.headers.authorization);
  return { user };
}

Авторизация в резолвере

Теперь любой резолвер может заглянуть в context.user и решить, можно ли отдавать данные:

const resolvers = {
  Query: {
    me: (_p, _a, ctx) => {
      if (!ctx.user) throw new GraphQLError("Не авторизован");
      return ctx.user;
    },
    allUsers: (_p, _a, ctx) => {
      if (ctx.user?.role !== "ADMIN")
        throw new GraphQLError("Только для админов");
      return ctx.db.users;
    },
  },
};

Как работает под капотом

Один и тот же объект context передаётся третьим аргументом во все резолверы этого запроса. Это и делает его идеальным местом для пользователя: проверять права можно на любом уровне дерева — хоть на корневом поле, хоть на вложенном (например, «показывать email только владельцу профиля»). Смоделируем защиту полей на JS:

const db = {
  users: [
    { id: 1, name: "Аня", email: "[email protected]", role: "USER" },
    { id: 2, name: "Боб", email: "[email protected]", role: "ADMIN" }
  ]
};

function resolveAllUsers(_p, _a, ctx) {
  if (!ctx.user) throw new Error("Не авторизован");
  if (ctx.user.role !== "ADMIN") throw new Error("Только для админов");
  return db.users;
}

// email видит только владелец или админ
function resolveEmail(parent, _a, ctx) {
  const own = ctx.user && ctx.user.id === parent.id;
  const admin = ctx.user && ctx.user.role === "ADMIN";
  return (own || admin) ? parent.email : null;
}

const ctxAdmin = { user: { id: 2, role: "ADMIN" } };
const ctxGuest = { user: null };

console.log(resolveAllUsers(null, null, ctxAdmin).length); // 2
try { resolveAllUsers(null, null, ctxGuest); }
catch (e) { console.log("Гость:", e.message); }            // Не авторизован
console.log("email чужого:", resolveEmail(db.users[0], null, ctxAdmin)); // виден админу

Попробуй сам ▶ — поменяй ctxAdmin на обычного пользователя и проверь доступ к allUsers и к чужому email. Так права живут на уровне отдельных полей.

Частые ошибки

  • Думать, что «скрытое поле» защищено. Если поле есть в схеме, его можно запросить. Защищает не отсутствие в UI, а проверка в резолвере.
  • Класть пользователя в глобальную переменную. Только context — иначе параллельные запросы перепутают, кто есть кто.
  • Авторизовать только корневые поля. Чувствительные вложенные поля (email, телефон) тоже нужно защищать на их уровне.

Best practices

  • Аутентификацию делай один раз в context, авторизацию — в резолверах, как можно ближе к защищаемым данным.
  • Бросай специальные ошибки (GraphQLError с кодом вроде UNAUTHENTICATED/FORBIDDEN), чтобы клиент мог их различать.
  • Для повторяющихся проверок выноси хелперы или директивы авторизации, чтобы не дублировать if (!ctx.user) по всем резолверам.

Итоги

Context создаётся на каждый запрос и несёт текущего пользователя; он передаётся во все резолверы. Аутентификация («кто ты») делается один раз при создании context, авторизация («что можно») — в резолверах, в том числе на уровне отдельных вложенных полей. Защищает не отсутствие поля в UI, а явная проверка прав. Дальше — главная проблема производительности: N+1 и DataLoader.

Проверьте себя
1. Где правильно проверять права доступа в GraphQL?
AПо URL эндпоинта
BВ резолверах, опираясь на context с текущим пользователем
CВ заголовках HTTP-ответа
DВ схеме SDL автоматически
2. В чём разница между аутентификацией и авторизацией?
AЭто одно и то же
BАутентификация — кто ты (раз в context), авторизация — что тебе можно (в резолверах)
CАвторизация быстрее
DАутентификация делается на клиенте