Лекция 11. Авторизация и безопасность веб-приложений
Раздел 4. Разработка интернет-приложений (backend, Python/FastAPI) Длительность пары: ~1 ч 30 мин
План лекции
- Аутентификация и авторизация: разграничение понятий
- Модели управления доступом: RBAC, ACL, ABAC
- Проверка прав в FastAPI: зависимости и защита маршрутов
- Типовые угрозы (OWASP) и защита от них
- CORS: зачем и как настраивать
- Заголовки безопасности, HTTPS и защита токенов
- Лучшие практики безопасности
- Сквозной пример защищённого API
1. Аутентификация и авторизация
Эти два термина часто путают, но они отвечают на разные вопросы.
- Аутентификация (Authentication) — проверка личности пользователя. Отвечает на вопрос: «Кто вы?». Реализуется через логин/пароль, токены, биометрию.
- Авторизация (Authorization) — определение прав доступа. Отвечает на вопрос: «Что вам разрешено делать?». Реализуется через роли, разрешения, списки доступа.
Порядок всегда такой: сначала система аутентифицирует пользователя (понимает, кто он), затем авторизует конкретное действие (решает, можно ли ему его выполнить). Аутентификации мы посвятили прошлую лекцию (сессии, JWT, OAuth 2.0). Сегодня сосредоточимся на авторизации и на защите приложения в целом.
Аналогия: аутентификация — это проверка паспорта на входе в здание. Авторизация — это пропуск, который открывает только те двери, к которым у вас есть доступ.
В HTTP этим двум этапам соответствуют разные коды ответа:
- 401 Unauthorized — пользователь не аутентифицирован (не предоставил или предоставил неверные учётные данные);
- 403 Forbidden — пользователь аутентифицирован, но не имеет прав на действие.
2. Модели управления доступом
2.1. RBAC (Role-Based Access Control)
RBAC — управление доступом на основе ролей. Пользователю назначается одна или несколько ролей, а роли связаны с набором разрешений (permissions). Проверяется не пользователь напрямую, а его роль.
Преимущества: централизованное управление, простота аудита, лёгкое масштабирование (новому сотруднику просто выдают роль).
from enum import Enumfrom fastapi import Depends, HTTPException, status
class Role(str, Enum): ADMIN = "admin" MODERATOR = "moderator" USER = "user"
class Permission(str, Enum): READ_BOOKS = "read:books" WRITE_BOOKS = "write:books" DELETE_BOOKS = "delete:books" MANAGE_USERS = "manage:users"
# Сопоставление ролей и разрешенийROLE_PERMISSIONS = { Role.ADMIN: [ Permission.READ_BOOKS, Permission.WRITE_BOOKS, Permission.DELETE_BOOKS, Permission.MANAGE_USERS, ], Role.MODERATOR: [ Permission.READ_BOOKS, Permission.WRITE_BOOKS, Permission.DELETE_BOOKS, ], Role.USER: [Permission.READ_BOOKS],}Связка «роль → разрешения» позволяет менять права целой группы пользователей, не трогая каждого по отдельности.
2.2. ACL (Access Control List)
ACL — список контроля доступа, привязанный к конкретному ресурсу. Для каждого объекта хранится перечень пользователей и того, что им разрешено. ACL отвечает на вопрос «кто и что может делать именно с этим объектом», тогда как RBAC оперирует ролями на уровне всей системы.
# ACL: для каждого ресурса — свой набор правresource_acl = { "book_123": { "owner": "user_1", "readers": ["user_2", "user_3"], "editors": ["user_2"], }}
def check_resource_access(resource_id: str, user_id: str, action: str) -> bool: acl = resource_acl.get(resource_id) if not acl: return False if acl["owner"] == user_id: return True # владелец имеет все права if action == "read" and user_id in acl.get("readers", []): return True if action == "write" and user_id in acl.get("editors", []): return True return FalseACL удобен там, где права индивидуальны для каждого объекта: документы в облаке, репозитории, общие папки.
2.3. ABAC (Attribute-Based Access Control)
ABAC — доступ на основе атрибутов пользователя, ресурса и контекста (время, IP, отдел). Это самая гибкая, но и самая сложная модель.
def check_access(user: dict, resource: dict, action: str) -> bool: if user["department"] == resource["department"]: return True if user["level"] >= 5 and action == "read": return True if resource["public"] and action == "read": return True return FalseКогда что выбирать:
| Модель | Гранулярность | Когда применять |
|---|---|---|
| RBAC | Роли | Большинство приложений, чёткая иерархия должностей |
| ACL | Отдельный объект | Индивидуальные права на ресурсы (файлы, документы) |
| ABAC | Атрибуты/контекст | Сложные политики, динамические условия |
На практике модели комбинируют: RBAC как основа плюс проверка владельца ресурса (элемент ACL).
3. Проверка прав в FastAPI
В FastAPI авторизация естественно выражается через систему зависимостей (Depends). Мы пишем функцию-проверку и подключаем её к маршруту. Если проверка не проходит, она бросает HTTPException, и обработчик маршрута даже не вызывается.
Предполагаем, что аутентификация уже даёт нам данные пользователя из токена (функция verify_token из прошлой лекции возвращает payload JWT с полем role).
3.1. Зависимость-фабрика для проверки разрешения
def require_permission(permission: Permission): def checker(payload: dict = Depends(verify_token)): user_role = Role(payload.get("role", "user")) allowed = ROLE_PERMISSIONS.get(user_role, []) if permission not in allowed: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Требуется разрешение '{permission.value}'", ) return payload return checkerrequire_permission — это фабрика зависимостей: она принимает нужное разрешение и возвращает функцию-проверку. Такой приём позволяет переиспользовать одну логику для разных прав.
3.2. Защита маршрута
@app.delete("/books/{book_id}")def delete_book( book_id: str, user: dict = Depends(require_permission(Permission.DELETE_BOOKS)),): return {"message": f"Книга {book_id} удалена"}Маршрут защищён: чтобы попасть внутрь, токен должен быть валидным (verify_token), а роль — содержать разрешение DELETE_BOOKS. Вся проверка прав вынесена из тела обработчика — он занимается только бизнес-логикой.
3.3. Проверка роли напрямую
Если хватает грубой проверки по роли, можно обойтись без таблицы разрешений:
def require_role(required: Role): def checker(payload: dict = Depends(verify_token)): if payload.get("role") != required.value: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав", ) return payload return checker
@app.get("/admin/stats")def admin_stats(user: dict = Depends(require_role(Role.ADMIN))): return {"users": 1024, "active": 768}Важно: авторизацию нужно проверять на сервере на каждом защищённом эндпойнте. Скрытие кнопки в интерфейсе — это удобство, а не защита: клиент всегда может отправить запрос напрямую.
4. Типовые угрозы и защита (OWASP)
OWASP (Open Web Application Security Project) — некоммерческий проект, публикующий рейтинг наиболее опасных уязвимостей веб-приложений (OWASP Top 10). Разберём ключевые угрозы.
4.1. Инъекции (SQL Injection)
Атака, при которой пользовательский ввод подставляется в запрос как код. Например, ввод ' OR '1'='1 в поле логина может обойти проверку.
Защита: параметризованные запросы и ORM — данные передаются отдельно от текста запроса.
# Опасно: данные склеиваются с текстом запросаquery = f"SELECT * FROM users WHERE username = '{username}'"
# Безопасно: параметр передаётся отдельноquery = "SELECT * FROM users WHERE username = %s"cursor.execute(query, (username,))4.2. XSS (Cross-Site Scripting)
Внедрение вредоносного JavaScript в страницу через непроверенный ввод. Когда другой пользователь открывает страницу, скрипт выполняется в его браузере (например, крадёт cookie).
Защита: экранирование вывода, заголовок Content-Security-Policy, хранение токенов в HttpOnly cookie (тогда JS не имеет к ним доступа).
from html import escape
user_input = "<script>alert('XSS')</script>"safe = escape(user_input) # <script>alert('XSS')</script>4.3. CSRF (Cross-Site Request Forgery)
Атака, при которой вредоносный сайт заставляет браузер жертвы отправить запрос на доверенный сайт, используя её действующую сессию (cookie прикрепляется браузером автоматически).
Защита: CSRF-токены, cookie с атрибутом SameSite=Strict/Lax. API на JWT в заголовке Authorization (а не в cookie) к CSRF устойчивы — браузер не подставляет такой заголовок автоматически.
4.4. Broken Authentication
Слабые места механизма входа: предсказуемые токены, отсутствие ограничения попыток, хранение паролей в открытом виде, бесконечно живущие сессии.
Защита: хеширование паролей (bcrypt/argon2), короткое время жизни access-токенов, refresh-токены, rate limiting на входе, MFA.
4.5. Brute Force
Перебор паролей или токенов. Защита: ограничение частоты запросов (rate limiting), CAPTCHA, временная блокировка после серии неудач.
from fastapi import FastAPI, Requestfrom slowapi import Limiter, _rate_limit_exceeded_handlerfrom slowapi.util import get_remote_addressfrom slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)app = FastAPI()app.state.limiter = limiterapp.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/login")@limiter.limit("5/minute") # не более 5 попыток в минуту с одного IPdef login(request: Request): ...4.6. Сводная таблица «угроза → защита»
| Угроза | Суть | Защита |
|---|---|---|
| SQL Injection | Внедрение SQL через ввод | Параметризованные запросы, ORM |
| XSS | Внедрение JS на страницу | Экранирование вывода, CSP, HttpOnly cookie |
| CSRF | Запрос от имени жертвы | CSRF-токены, SameSite cookie, JWT в заголовке |
| Broken Auth | Слабая аутентификация | Хеширование, короткие токены, MFA, rate limit |
| Brute Force | Перебор паролей | Rate limiting, CAPTCHA, блокировка |
| Session Hijacking | Кража сессии/токена | HTTPS, Secure/HttpOnly cookie, ротация токенов |
| Sensitive Data Exposure | Утечка данных | HTTPS, шифрование, не логировать секреты |
5. CORS: зачем и как настраивать
5.1. Почему это нужно
Браузеры реализуют Same-Origin Policy (политику одного источника): по умолчанию JavaScript со страницы https://app.example.com не может делать запросы к https://api.other.com. Источник (origin) определяется тройкой «схема + домен + порт».
CORS (Cross-Origin Resource Sharing) — механизм, которым сервер разрешает определённым «чужим» источникам обращаться к нему. Сервер добавляет в ответ заголовки (Access-Control-Allow-Origin и др.), и браузер на их основании решает, пропустить ответ в код страницы или заблокировать.
Важно: CORS — это защита браузера, а не сервера. Запросы от других программ (curl, мобильное приложение) она не ограничивает.
5.2. Настройка в FastAPI
from fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], # конкретные источники allow_credentials=True, # разрешить cookie/заголовки авторизации allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Authorization", "Content-Type"],)Антипаттерн:
allow_origins=["*"]вместе сallow_credentials=True. Это небезопасно, и спецификация такую комбинацию запрещает. Всегда указывайте явный список доверенных источников для приложений с авторизацией.
При «нетривиальных» запросах браузер сначала шлёт preflight-запрос методом OPTIONS, чтобы узнать разрешённые методы и заголовки. CORSMiddleware обрабатывает его автоматически.
6. Заголовки безопасности, HTTPS и защита токенов
6.1. Заголовки безопасности
Несколько HTTP-заголовков ответа существенно повышают защищённость практически бесплатно:
@app.middleware("http")async def add_security_headers(request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" # не угадывать MIME-тип response.headers["X-Frame-Options"] = "DENY" # запрет встраивания в iframe (clickjacking) response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" # только HTTPS response.headers["Content-Security-Policy"] = "default-src 'self'" # источники ресурсов return responseДополнительно полезно ограничить разрешённые домены через TrustedHostMiddleware, чтобы защититься от подмены заголовка Host.
6.2. HTTPS
Весь трафик с учётными данными и токенами должен идти только по HTTPS. Без шифрования пароль или JWT можно перехватить в открытом виде (атака «человек посередине»). Заголовок Strict-Transport-Security (HSTS) заставляет браузер всегда использовать HTTPS для домена.
6.3. Защита токенов
- Передавайте токены в заголовке
Authorization: Bearer <token>, не в URL (URL попадают в логи и историю браузера). - Для cookie с токеном используйте флаги
Secure(только по HTTPS),HttpOnly(недоступно из JS) иSameSite. - Делайте срок жизни access-токена коротким (минуты—десятки минут), а для продления используйте refresh-токены.
- Секретный ключ подписи и параметры конфигурации храните в переменных окружения, а не в коде.
from pydantic_settings import BaseSettings
class Settings(BaseSettings): secret_key: str database_url: str jwt_algorithm: str = "HS256" access_token_expire_minutes: int = 30
class Config: env_file = ".env" # .env обязательно добавить в .gitignore
settings = Settings()7. Лучшие практики безопасности
- Принцип наименьших привилегий (Least Privilege): пользователь и сервис получают только тот минимум прав, который нужен для работы, и ничего сверх.
- Валидация входных данных: проверяйте всё, что приходит от клиента. В FastAPI это естественно делается через Pydantic-модели (типы, ограничения, форматы).
- Не доверяйте клиенту: любые проверки прав и данных дублируйте на сервере, даже если они есть в интерфейсе.
- Ограничение частоты (rate limiting): защищает вход, регистрацию и тяжёлые эндпойнты от перебора и DoS.
- Хеширование паролей: только bcrypt/argon2/scrypt с солью, никогда не MD5/SHA-1 и не открытый текст.
- Логирование и аудит: записывайте попытки входа (успешные и нет) и действия, требующие авторизации, но не логируйте секреты (пароли, токены).
- Хранение секретов: переменные окружения или менеджеры секретов,
.env— в.gitignore. - Обновление зависимостей: уязвимости часто приходят через устаревшие библиотеки; регулярно проверяйте их (
pip-audit). - Не изобретайте криптографию: используйте проверенные библиотеки и стандарты.
8. Сквозной пример защищённого API
Соберём вместе аутентификацию (JWT) и авторизацию (RBAC) в одном приложении.
from fastapi import FastAPI, Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestFormfrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModelfrom datetime import datetime, timedeltafrom enum import Enumimport jwtimport bcrypt
app = FastAPI(title="Secure API")oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)
SECRET_KEY = "change-me-in-production" # в реальности — из переменных окруженияALGORITHM = "HS256"EXPIRE_MINUTES = 30
class Role(str, Enum): ADMIN = "admin" USER = "user"
class User(BaseModel): username: str role: Role hashed_password: str
users_db: dict[str, User] = {} # в реальности — БД (PostgreSQL)
# --- работа с паролями ---def hash_password(p: str) -> str: return bcrypt.hashpw(p.encode(), bcrypt.gensalt()).decode()
def verify_password(p: str, hashed: str) -> bool: return bcrypt.checkpw(p.encode(), hashed.encode())
# --- работа с токенами ---def create_access_token(data: dict) -> str: payload = data.copy() payload["exp"] = datetime.utcnow() + timedelta(minutes=EXPIRE_MINUTES) return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(token: str = Depends(oauth2_scheme)) -> User: error = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Не удалось проверить учётные данные", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") except jwt.InvalidTokenError: raise error user = users_db.get(username) if user is None: raise error return user
def require_role(required: Role): def checker(user: User = Depends(get_current_user)) -> User: if user.role != required: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав", ) return user return checker
# --- эндпойнты ---@app.post("/token")def login(form: OAuth2PasswordRequestForm = Depends()): user = users_db.get(form.username) if not user or not verify_password(form.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Неверный логин или пароль", headers={"WWW-Authenticate": "Bearer"}, ) token = create_access_token({"sub": user.username, "role": user.role.value}) return {"access_token": token, "token_type": "bearer"}
@app.get("/users/me")def read_me(user: User = Depends(get_current_user)): return {"username": user.username, "role": user.role}
@app.get("/admin/users")def list_users(admin: User = Depends(require_role(Role.ADMIN))): return [{"username": u.username, "role": u.role} for u in users_db.values()]Что здесь обеспечивает безопасность:
- пароли хранятся только в виде bcrypt-хеша (
hash_password); - доступ к данным даёт лишь валидный JWT (
get_current_user) — это аутентификация; - эндпойнт
/admin/usersдополнительно требует рольADMIN(require_role) — это авторизация; - CORS ограничен доверенным источником;
- секрет и параметры вынесены в константы (в продакшене — в переменные окружения);
- ответы не содержат
hashed_password.
Краткие итоги
- Аутентификация отвечает на вопрос «кто вы», авторизация — «что вам разрешено». Коды: 401 — не аутентифицирован, 403 — нет прав.
- RBAC (роли и разрешения) — основная модель для большинства приложений; ACL удобен для прав на отдельные объекты; ABAC — для гибких политик по атрибутам.
- В FastAPI авторизация выражается через зависимости (
Depends) и фабрики проверок; права проверяются на сервере на каждом защищённом маршруте. - Знание OWASP Top 10 (инъекции, XSS, CSRF, broken auth, brute force) и типовых мер защиты — обязательная база.
- CORS разрешает доверенным источникам обращаться к API; не используйте
*вместе сallow_credentials. - HTTPS, заголовки безопасности,
HttpOnly/Securecookie и короткоживущие токены защищают данные и сессии. - Безопасность строится на принципах: наименьшие привилегии, валидация ввода, rate limiting, логирование, хранение секретов вне кода.
Вопросы для самопроверки
- В чём разница между аутентификацией и авторизацией? Какому из этих этапов соответствуют коды 401 и 403?
- Чем модель RBAC отличается от ACL? Приведите пример задачи для каждой.
- Как в FastAPI с помощью
Dependsограничить доступ к маршруту по роли? Зачем нужна «фабрика зависимостей»? - Что такое SQL-инъекция и почему параметризованные запросы её предотвращают?
- Чем XSS отличается от CSRF? Какие меры защищают от каждой из угроз?
- Зачем нужен CORS и почему он считается защитой браузера, а не сервера? Какая комбинация настроек небезопасна?
- Какие заголовки безопасности вы знаете и от чего защищает каждый?
- Почему access-токен делают короткоживущим и где безопаснее хранить токены?
- Сформулируйте принцип наименьших привилегий на конкретном примере.
- Какие практики помогают защитить эндпойнт входа от перебора паролей?