Перейти к содержимому

Лекция 11. Авторизация и безопасность веб-приложений

Раздел 4. Разработка интернет-приложений (backend, Python/FastAPI) Длительность пары: ~1 ч 30 мин


План лекции

  1. Аутентификация и авторизация: разграничение понятий
  2. Модели управления доступом: RBAC, ACL, ABAC
  3. Проверка прав в FastAPI: зависимости и защита маршрутов
  4. Типовые угрозы (OWASP) и защита от них
  5. CORS: зачем и как настраивать
  6. Заголовки безопасности, HTTPS и защита токенов
  7. Лучшие практики безопасности
  8. Сквозной пример защищённого 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 Enum
from 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 False

ACL удобен там, где права индивидуальны для каждого объекта: документы в облаке, репозитории, общие папки.

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 checker

require_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) # &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;

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, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post("/login")
@limiter.limit("5/minute") # не более 5 попыток в минуту с одного IP
def 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 FastAPI
from 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, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from datetime import datetime, timedelta
from enum import Enum
import jwt
import 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/Secure cookie и короткоживущие токены защищают данные и сессии.
  • Безопасность строится на принципах: наименьшие привилегии, валидация ввода, rate limiting, логирование, хранение секретов вне кода.

Вопросы для самопроверки

  1. В чём разница между аутентификацией и авторизацией? Какому из этих этапов соответствуют коды 401 и 403?
  2. Чем модель RBAC отличается от ACL? Приведите пример задачи для каждой.
  3. Как в FastAPI с помощью Depends ограничить доступ к маршруту по роли? Зачем нужна «фабрика зависимостей»?
  4. Что такое SQL-инъекция и почему параметризованные запросы её предотвращают?
  5. Чем XSS отличается от CSRF? Какие меры защищают от каждой из угроз?
  6. Зачем нужен CORS и почему он считается защитой браузера, а не сервера? Какая комбинация настроек небезопасна?
  7. Какие заголовки безопасности вы знаете и от чего защищает каждый?
  8. Почему access-токен делают короткоживущим и где безопаснее хранить токены?
  9. Сформулируйте принцип наименьших привилегий на конкретном примере.
  10. Какие практики помогают защитить эндпойнт входа от перебора паролей?