Защита GraphQL API с помощью JWT

GraphQL — альтернатива классическим REST API, позволяющая клиентам гибко запрашивать только нужные поля. Эта гибкость усиливает удобство, но одновременно увеличивает поверхность для атак, особенно в частях, касающихся контроля доступа. Одним из распространённых и практичных способов обеспечить аутентификацию и авторизацию является использование JSON Web Tokens (JWT).
Важно: JWT — это формат токена для передачи утверждений (claims). Он сам по себе не делает приложение безопасным — это инструмент, который нужно правильно внедрить.
Что вы получите из этого руководства
- Как настроить Express.js + Apollo Server для GraphQL.
- Как хранить пользователей в MongoDB через Mongoose.
- Как генерировать и верифицировать JWT, интегрируя их в контекст резолверов.
- Лучшие практики безопасности: хеширование паролей, срок действия токенов, отзыв токенов, защита от перегрузки запросами.
- Тесты, критерии приёмки и чек-листы для ролей.
Основные термины в 1 строке
- JWT: компактный токен для передачи утверждений между сторонами.
- Резолвер: функция, возвращающая данные для поля схемы GraphQL.
- Контекст (context): объект, доступный всем резолверам для хранения информации запроса (заголовки, пользователь и т. п.).
Аутентификация и авторизация в GraphQL API
В отличие от REST, GraphQL обычно имеет одну точку входа. Клиенты могут запрашивать переменное количество данных — это удобно, но усложняет разграничение доступа. Типичные риски: отсутствие проверки прав на уровне полей, «over-fetching» чувствительной информации и broken access control.
Рекомендуется сочетать аутентификацию (кто вы) и авторизацию (что вам разрешено). JWT обычно используются для аутентификации; авторизация — логика в резолверах, посредниках или на уровне схемы.
Важно: всегда храните токен и секрет аккуратно и не сохраняйте пароли в виде открытого текста (см. раздел «Безопасность» ниже).
Источник кода проекта доступен в репозитории проекта (укажите свой репозиторий при публикации).
Установка Express.js + Apollo Server
Apollo Server — популярная реализация GraphQL-сервера. Он помогает быстро определить схемы, резолверы и подключить источники данных.
Создайте папку проекта и инициализируйте npm:
mkdir graphql-API-jwt
cd graphql-API-jwt
npm init --yesУстановите зависимости:
npm install apollo-server graphql mongoose jsonwebtoken dotenv expressПримечание: в оригинале был пакет “jsonwebtokens” — правильное имя пакета для JWT в Node.js — “jsonwebtoken”.
Создайте файл server.js в корне и настройте сервер:
const { ApolloServer } = require('apollo-server');
const mongoose = require('mongoose');
require('dotenv').config();
const typeDefs = require("./graphql/typeDefs");
const resolvers = require("./graphql/resolvers");
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({ req }),
});
const MONGO_URI = process.env.MONGO_URI;
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Connected to DB");
return server.listen({ port: 5000 });
})
.then((res) => {
console.log(`Server running at ${res.url}`);
})
.catch(err => {
console.log(err.message);
});Контекст сервера прокидывает объект req в резолверы, что позволяет проверять заголовки запросов и извлекать токены.
Подключение MongoDB
Создайте базу данных локально или используйте MongoDB Atlas. В корне проекта создайте файл .env и добавьте строку подключения:
MONGO_URI="" И храните секрет для подписи JWT тоже в .env:
SECRET_KEY="" Модель данных пользователя
Создайте models/user.js:
const { model, Schema } = require('mongoose');
const userSchema = new Schema({
name: String,
password: String,
role: String
});
module.exports = model('user', userSchema);Важно: в продакшне никогда не храните пароли в открытом виде. Всегда используйте устойчивое хеширование (bcrypt, Argon2) и соль.
Схема GraphQL
В папке graphql создайте typeDefs.js:
const { gql } = require("apollo-server");
const typeDefs = gql`
type User {
id: ID!
name: String!
password: String!
role: String!
}
input UserInput {
name: String!
password: String!
role: String!
}
type TokenResult {
message: String
token: String
}
type Query {
users: [User]
}
type Mutation {
register(userInput: UserInput): User
login(name: String!, password: String!): TokenResult
}
`;
module.exports = typeDefs;Комментарий: поле password обычно не возвращается клиенту. В схеме выше оно присутствует для совместимости с примером, но в реальном API не включайте пароль в type User.
Резолверы и JWT: генерация и проверка
Добавьте в graphql/resolvers.js логику для генерации/проверки JWT и сами резолверы.
const User = require("../models/user");
const jwt = require('jsonwebtoken');
const secretKey = process.env.SECRET_KEY;
function generateToken(user) {
const token = jwt.sign(
{ id: user.id, role: user.role },
secretKey,
{ expiresIn: '1h', algorithm: 'HS256' }
);
return token;
}
function verifyToken(token) {
if (!token) {
throw new Error('Token not provided');
}
try {
const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'] });
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}
const resolvers = {
Mutation: {
register: async (_, { userInput: { name, password, role } }) => {
if (!name || !password || !role) {
throw new Error('Name, password, and role required');
}
const newUser = new User({
name: name,
password: password,
role: role,
});
try {
const response = await newUser.save();
return {
id: response._id,
...response._doc,
};
} catch (error) {
console.error(error);
throw new Error('Failed to create user');
}
},
login: async (_, { name, password }) => {
try {
const user = await User.findOne({ name: name });
if (!user) {
throw new Error('User not found');
}
if (password !== user.password) {
throw new Error('Incorrect password');
}
const token = generateToken(user);
if (!token) {
throw new Error('Failed to generate token');
}
return {
message: 'Login successful',
token: token,
};
} catch (error) {
console.error(error);
throw new Error('Login failed');
}
}
},
Query: {
users: async (parent, args, context) => {
try {
const token = context.req.headers.authorization || '';
const decodedToken = verifyToken(token);
if (decodedToken.role !== 'Admin') {
throw new Error('Unauthorized. Only Admins can access this data.');
}
const users = await User.find({}, { name: 1, _id: 1, role: 1 });
return users;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch users');
}
},
},
};
module.exports = resolvers;Важные замечания к примеру выше:
- Пароли сравниваются напрямую — это уязвимо. Внедрите bcrypt: храните только хеш и сравнивайте через bcrypt.compare.
- Токен берётся из заголовка Authorization. Часто используется формат “Bearer
” — удаляйте префикс перед верификацией. - Для отказа от токена (ревока) можно использовать список отозванных токенов (черный список) или хранить у пользователя токен-идентификатор.
Запуск сервера и тестирование
Запустите сервер:
node server.jsОткройте Apollo Server sandbox (или GraphQL Playground / GraphiQL) по указанному адресу. Протестируйте:
- mutation register — создать пользователя.
- mutation login — получить токен.
- Добавьте токен в поле авторизации: часто это заголовок Authorization: Bearer
. - Сделайте query users — убедитесь, что доступ есть только у Admin.
Безопасность: рекомендации и харднинг
Important: базовые механизмы аутентификации и авторизации — необходимы, но недостаточны. Ниже — набор практических мер.
- Хешируйте пароли: используйте bcrypt или Argon2. Никогда не храните пароли в открытом виде.
- Используйте HTTPS: передача токенов и паролей по незашифрованному каналу недопустима.
- Устанавливайте разумный срок жизни токена (например, 1 час) и используйте Refresh-токены, хранящиеся безопасно.
- Защита от CSRF: если храните токен в cookie, обеспечьте защиту CSRF-токеном.
- Ограничение глубины и сложности запросов: внедрите лимит глубины (query depth limit) и лимит на общую стоимость запроса (query cost analysis).
- Rate limiting: ограничивайте количество запросов с одного IP/пользователя.
- Логирование и мониторинг: фиксируйте неудачные попытки аутентификации и аномалии.
- Validation: валидируйте ввод на уровне резолверов и/или схемы; проверяйте типы и допустимые значения.
- Минимизация данных: отдавайте минимально необходимую информацию — не включайте пароли и прочие секреты в ответы.
- Ревок токенов: для возможности отзыва токенов держите версию токена в профиле пользователя (tokenVersion) или чёрный список.
- Хранение секретов: используйте менеджер секретов (Vault, AWS Secrets Manager) или управляемые переменные окружения.
Альтернативные подходы
- Session-based authentication (сессии на сервере) — проще реализовать отзыв токенов, но сложнее масштабировать без централизованного хранилища сессий.
- OAuth2 / OpenID Connect — для федеративной аутентификации и SSO.
- API Gateways / прокси — вынесение аутентификации/авторизации в слой API Gateway для унификации и защиты точек входа.
Когда JWT не подходит (контрпримеры)
- Когда требуется мгновенный отзыв токенов для большого количества пользователей и нет инфраструктуры для чёрного списка.
- Когда политика безопасности требует хранить сессии исключительно на сервере.
- Для короткоживущих одноразовых операций, где проще использовать временные одноразовые коды.
Мини‑методология внедрения JWT (шаги)
- Проектирование: решите, какие данные попадут в payload token (id, role, tokenVersion).
- Секреты: выберите алгоритм (HS256/RS256). Для RS256 нужен парный приватный/публичный ключ.
- Хеширование паролей: мигрируйте существующие пароли на безопасный хеш.
- Контекст: прокиньте req в context каждого резолвера.
- Валидация: добавьте проверку формата и лимитов запросов.
- Тесты: покройте сценарии логина, доступа, отказа и ревока.
- Мониторинг и аудит: логируйте подозрительные события.
Чек-листы по ролям
Разработчик:
- Реализовал генерацию и верификацию JWT.
- Использует хеширование паролей.
- Не возвращает пароль в ответах.
- Обрабатывает формат “Bearer
”.
Security Engineer / DevSecOps:
- Настроил хранение SECRET_KEY в безопасном хранилище.
- Включил HTTPS и проверил конфигурацию TLS.
- Добавил лимиты глубины и стоимости запросов.
- Настроил ревок токенов/черный список.
Ops / SRE:
- Наблюдает за метриками аутентификации (ошибки, латентность).
- Настроил алерты на всплески неудачных попыток логина.
- Обеспечил безопасное резервное копирование БД.
Тесты и критерии приёмки
Критерии приёмки:
- Регистрация создаёт пользователя в базе с хешированным паролем.
- Логин с корректными учётными данными возвращает JWT.
- Запрос users без токена — 401/ошибка “Token not provided”.
- Запрос users с токеном не-Admin — возвращается ошибка авторизации.
- Запрос users с токеном Admin — возвращает список пользователей без паролей.
Тестовые сценарии (автоматизируемые):
- TC-01: Успешная регистрация нового пользователя.
- TC-02: Попытка регистрации с пустыми полями — 400/ошибка.
- TC-03: Успешный логин и получение токена.
- TC-04: Использование просроченного токена — ошибка “Invalid token”.
- TC-05: Попытка доступа к users с ролью User — отказ.
Миграция и совместимость
При миграции существующих систем:
- Добавьте флаг миграции для старых пользователей, чтобы принудить смену пароля и хеширование.
- Внедрите постепенное требование refresh-токенов, если ранее использовались долгоживущие токены.
Конфиденциальность и соответствие (GDPR)
- Минимизируйте персональные данные, возвращая только необходимые поля.
- Обеспечьте возможность удаления/анонимизации данных по запросу пользователя.
- Логируйте доступ к чувствительным данным и храните логи в соответствии с политикой хранения.
Короткий план реагирования при инциденте с токенами
- Отключить службу аутентификации или временно увеличить контроль доступа.
- Ревокнуть актуальные токены (увеличить tokenVersion или занести в черный список).
- Проинформировать пользователей, при необходимости инициировать принудительную смену паролей.
- Провести аудит логов и исправить уязвимость.
- Выпустить обновление и инструкцию для пользователей.
Итог и рекомендации
Аутентификация и авторизация с помощью JWT — удобный и распространённый подход для GraphQL API, особенно в сочетании с Apollo Server. Однако безопасность — это многослойная задача: токены должны корректно генерироваться и валидироваться, пароли — хешироваться, а инфраструктура — защищаться от злоупотреблений (rate limiting, depth limiting, HTTPS и т. п.).
Рекомендации по приоритету внедрения:
- Хеширование паролей (срочно).
- Введение HTTPS и безопасного хранения секретов.
- Настройка срока жизни токенов и стратегия ревока.
- Лимиты запросов и защита от глубоких/дорогих запросов.
Короткая заметка: начните с простого рабочего прототипа, затем постепенно внедряйте дополнительные слои защиты и покрывайте всё автоматизированными тестами.
Summary:
- JWT подходят для аутентификации, но требуют дополнительных мер (ревок, харднинг).
- Не храните пароли в открытом виде; используйте bcrypt/Argon2.
- Контекст GraphQL удобен для передачи информации о запросе в резолверы.
- Тесты и мониторинг — критичные элементы надёжности.
Important: перед выводом в продакшн пройдите независимый аудит безопасности и нагрузочное тестирование.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone