Контекст и авторизация в резолверах
Объект 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.