Аутентификация в Flask с помощью JWT
Простая и надёжная аутентификация в Flask реализуется с помощью JSON Web Tokens (JWT). В этой статье показано, как настроить проект Flask с MongoDB, зарегистрировать пользователей, вход (login), защищённые маршруты и как улучшить безопасность: истечение срока токена, безопасные cookie, отзыв токенов и лучшие практики для продакшена.
Зачем использовать токены и что такое JWT
Сломанная аутентификация остаётся одной из серьёзных уязвимостей у веб-приложений и API. Токен‑базированная аутентификация использует зашифрованную строку (токен) для проверки и авторизации доступа к ресурсам. JWT — компактный, самодостаточный формат токена, который безопасно переносит удостоверения между клиентом и сервером.

Ключевые свойства JWT:
- Состоит из трёх частей: header, payload, signature.
- Payload содержит claims — полезные данные: user_id, роли, срок действия.
- Подпись гарантирует целостность токена и защищает от подделки.
Факты
- JWT — текстовый формат, обычно кодируется Base64URL.
- Стандарт определён в RFC 7519.
Важно: JWT сам по себе не шифрует данные, он их подписывает. Не храните в payload чувствительные данные без дополнительного шифрования.
Подготовка проекта Flask и MongoDB
Шаги, которые описаны ниже, помогут настроить окружение и зависимости.
Создайте директорию проекта и виртуальное окружение:
mkdir flask-project
cd flask-projectУстановите virtualenv и создайте окружение:
virtualenv venvАктивируйте виртуальное окружение:
# Unix or MacOS:
source venv/bin/activate
# Windows:
.\venv\Scripts\activateВ корне создайте requirements.txt со следующими зависимостями:
flask
pyjwt
python-dotenv
pymongo
bcryptУстановите зависимости:
pip install -r requirements.txtСоздайте MongoDB (локально или в облаке MongoDB Atlas), затем добавьте строку подключения в файл .env:
MONGO_URI="" Пример модуля подключения к базе (utils/db.py):
from pymongo import MongoClient
def connect_to_mongodb(mongo_uri):
client = MongoClient(mongo_uri)
db = client.get_database("users")
return dbОписание: функция создаёт клиент MongoDB и возвращает объект базы данных. Если коллекция ещё не создана, MongoDB создаст её при первой вставке.
Создайте основной файл приложения app.py:
from flask import Flask
from routes.user_auth import register_routes
from utils.db import connect_to_mongodb
import os
from dotenv import load_dotenv
app = Flask(__name__)
load_dotenv()
mongo_uri = os.getenv('MONGO_URI')
db = connect_to_mongodb(mongo_uri)
register_routes(app, db)
if __name__ == '__main__':
app.run(debug=True)Модель пользователя
Создайте модель пользователя (models/user_model.py):
from pymongo.collection import Collection
from bson.objectid import ObjectId
class User:
def __init__(self, collection: Collection, username: str, password: str):
self.collection = collection
self.username = username
self.password = password
def save(self):
user_data = {
'username': self.username,
'password': self.password
}
result = self.collection.insert_one(user_data)
return str(result.inserted_id)
@staticmethod
def find_by_id(collection: Collection, user_id: str):
return collection.find_one({'_id': ObjectId(user_id)})
@staticmethod
def find_by_username(collection: Collection, username: str):
return collection.find_one({'username': username})Модель предоставляет базовые операции: сохранение и поиск пользователя по id или username.
Маршруты аутентификации (routes/user_auth.py)
Ниже приведён минимальный вариант маршрутов регистрации, логина и защищённого доступа. Код сохранён из примера проекта — он работает, но требует усиления для продакшена.
import jwt
from functools import wraps
from flask import jsonify, request, make_response
from models.user_model import User
import bcrypt
import os
def register_routes(app, db):
collection = db.users
app.config['SECRET_KEY'] = os.urandom(24)
@app.route('/api/register', methods=['POST'])
def register():
username = request.json.get('username')
password = request.json.get('password')
existing_user = User.find_by_username(collection, username)
if existing_user:
return jsonify({'message': 'Username already exists!'})
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
new_user = User(collection, username, hashed_password.decode('utf-8'))
user_id = new_user.save()
return jsonify({'message': 'User registered successfully!', 'user_id': user_id})Добавьте маршрут логина:
@app.route('/api/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.find_by_username(collection, username)
if user:
if bcrypt.checkpw(password.encode('utf-8'), user['password'].encode('utf-8')):
token = jwt.encode({'user_id': str(user['_id'])}, app.config['SECRET_KEY'], algorithm='HS256')
response = make_response(jsonify({'message': 'Login successful!'}))
response.set_cookie('token', token)
return response
return jsonify({'message': 'Invalid username or password'})Декоратор для проверки токена:
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return jsonify({'message': 'Token is missing!'}) , 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = User.find_by_id(collection, data['user_id'])
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired!'}) , 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token!'}) , 401
return f(current_user, *args, **kwargs)
return decoratedИ защищённый маршрут для получения списка пользователей:
@app.route('/api/users', methods=['GET'])
@token_required
def get_users(current_user):
users = list(collection.find({}, {'_id': 0}))
return jsonify(users)Запустите сервер разработки:
flask runТестирование: используйте Postman или curl для отправки POST на /api/register и /api/login, затем GET на /api/users с кукой token.
Улучшения и практические рекомендации для продакшена
Приведённый выше код работает, но в реальном проекте требуется дополнительная защита. Ниже — список улучшений, которые стоит применить.
- Храните SECRET_KEY в окружении, а не генерируйте при старте приложения. Генерация новой ключа каждый запуск делает все токены недействительными при рестарте.
Пример в .env:
SECRET_KEY="" И в app.py:
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')- Добавьте срок жизни токена (exp) и используйте refresh tokens. Пример генерации токена с истечением:
import datetime
def create_access_token(user_id, secret, expires_minutes=15):
payload = {
'user_id': str(user_id),
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=expires_minutes)
}
return jwt.encode(payload, secret, algorithm='HS256')- Безопасные cookie: при установке куки укажите HttpOnly, Secure и SameSite для защиты от XSS/CSRF:
response.set_cookie('token', token, httponly=True, secure=True, samesite='Strict')Реализация механизма отзыва токенов (blacklist) или хранение идентификатора сессии в БД. Если требуется принудительный разлогин, заносите jti (уникальный идентификатор токена) в таблицу отозванных токенов.
Ограничьте количество попыток входа (rate limiting) и используйте капчу при необходимости.
Всегда используйте HTTPS в продакшене.
Минимизируйте данные в payload. Не храните пароли, секреты или PII (личные данные) в токене.
Логи безопасности: фиксируйте неудачные попытки входа, истечения токенов, и массовые ошибки аутентификации.
При хранении паролей используйте bcrypt с достаточным work factor (cost). Текущая библиотека bcrypt в Python делает это правильно, но настройте раундов по требованиям инфраструктуры.
CORS: ограничьте домены, которые могут обращаться к API.
Когда JWT может не подойти: контрпримеры
- Очень короткие сессии с частым изменением прав доступа. Потому что JWT сам по себе — статичный токен до истечения срока; смена прав потребует отзыв токена.
- Необходимость незамедлительной инвалидизации токенов без централизованного хранилища отзыва. В таких случаях лучше использовать серверные сессии.
- Ограничения по размеру токена при передаче по URL или в заголовках.
Альтернативы: серверные сессии (сессии в Redis), OAuth 2.0 (для сторонних авторизаций), API keys (для сервисов), mutual TLS (для высокобезопасных сервисов).
Методология внедрения (мини‑метод)
- Разработать минимально рабочую реализацию (MVP) с JWT и безопасными cookie.
- Добавить срок жизни токена и refresh tokens.
- Внедрить хранение jti в БД для возможности отзыва.
- Провести нагрузочное и безопасностное тестирование.
- Откат и улучшения: мониторинг, логирование, алерты.
Роли и чеклисты
Разработчик:
- Хранит SECRET_KEY в окружении.
- Добавил exp и jti в payload.
- Установил HttpOnly/Secure/SameSite для cookie.
- Использует bcrypt для хэширования пароля.
DevOps / SRE:
- Включил HTTPS и HSTS.
- Настроил резервное копирование базы данных.
- Обеспечил мониторинг аутентификаций и метрик ошибок.
Команда безопасности:
- Провела аудит токенов и проверила риски XSS/CSRF.
- Настроила политику отказа доступа и процедуру отзыва токенов.
Примеры тест-кейсов и критерии приёмки
Критерии приёмки:
- Регистрация нового пользователя возвращает user_id.
- Успешный логин отдаёт токен и устанавливает cookie.
- Защищённый маршрут /api/users доступен только с действительным токеном.
- Токен с истёкшим exp отказывается с 401.
- Попытки входа с неверным паролем не раскрывают, существует ли пользователь.
Тест-кейсы (кратко):
- Регистрация: POST /api/register с уникальным username — ожидается 200 и user_id.
- Логин: POST /api/login с правильными данными — ожидается кука token.
- Доступ защищённого ресурса: GET /api/users с кукой token — ожидается список пользователей.
- Просроченный токен: эмулировать exp в прошлом — ожидается 401.
Безопасность и конфигурация (жёсткие рекомендации)
- SECRET_KEY должен быть длиной минимум 32 байта и храниться вне репозитория.
- Настройте HTTPS везде, где передаёте токены.
- HttpOnly = True защищает от доступа JavaScript к куке.
- Secure = True передаёт куку только по HTTPS.
- SameSite = ‘Lax’ или ‘Strict’ снижает риск CSRF.
- Рассмотрите хранение токена в памяти клиента (например, при SPA) с дополнительной защитой от XSS.
Совместимость и миграция
Если у вас сейчас серверные сессии и вы переходите на JWT:
- План миграции: поддержка параллельных механизмов (dual auth) на время переноса.
- Создайте период, когда оба механизма валидны, затем постепенно выключайте старые сессии.
Шаблон ответа API (рекомендация)
- При успехе возвращайте минимально необходимые данные (message, user_id, token как кука).
- При ошибке используйте однотипную структуру: {“error”: “описание”, “code”: “AUTH_INVALID”}.
Быстрая проверка через curl
Регистрация:
curl -X POST http://localhost:5000/api/register -H "Content-Type: application/json" -d '{"username":"test","password":"pass"}'Логин (и сохранение cookie в файле):
curl -i -c cookies.txt -X POST http://localhost:5000/api/login -H "Content-Type: application/json" -d '{"username":"test","password":"pass"}'Доступ к защищённому маршруту используя сохранённые куки:
curl -b cookies.txt http://localhost:5000/api/usersПример улучшенного декоратора с проверкой exp и blacklist
# Примерный код для проверки токена с blacklist
from flask import jsonify, request
import jwt
from functools import wraps
blacklist = set() # в продакшене — коллекция в БД
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return jsonify({'message': 'Token is missing!'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if data.get('jti') in blacklist:
return jsonify({'message': 'Token revoked!'}), 401
current_user = User.find_by_id(db.users, data['user_id'])
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired!'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token!'}), 401
return f(current_user, *args, **kwargs)
return decoratedКраткое резюме
JSON Web Tokens — удобный и эффективный инструмент для аутентификации в Flask. Однако важно сочетать JWT с надёжными практиками безопасности: хранение секретов вне кода, срок действия токенов, безопасные cookie, возможность отзыва токенов и мониторинг. Следуя приведённым рекомендациям, вы уменьшите риск компрометации и подготовите систему к продакшен‑нагрузкам.
Сводка:
- JWT удобны для stateless аутентификации, но требуют дополнительных мер безопасности.
- Используйте exp, jti, безопасные cookie и храните SECRET_KEY в окружении.
- Рассмотрите refresh tokens и механизм отзыва для контроля сессий.
Важно: перед деплоем проверьте все механизмы на тестовом окружении и проведите security review.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone