Прямой ответ
Presigned URL — это временная ссылка с криптографической подписью, которая разрешает одно конкретное действие с одним объектом в течение заданного времени. Клиент загружает файл напрямую в объектное хранилище, минуя сервер приложения, поэтому трафик и память приложения в загрузке не участвуют.
Схема работы:
- Клиент запрашивает у приложения разрешение на загрузку.
- Приложение проверяет права, формирует ключ объекта и выдаёт подписанный URL.
- Клиент выполняет
PUTпо этому URL. - Хранилище проверяет подпись и срок, принимает файл.
- Клиент сообщает приложению о завершении, либо приложение проверяет наличие объекта.
import boto3
from botocore.config import Config
s3 = boto3.client("s3", config=Config(signature_version="s3v4"))
def issue_upload(user_id: str, filename: str) -> dict:
key = f"uploads/{user_id}/{uuid4().hex}-{filename}"
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={
"Bucket": "app-uploads",
"Key": key,
"ContentType": "application/pdf",
},
ExpiresIn=300, # 5 минут
)
return {"upload_url": url, "key": key}
Подпись покрывает метод, бакет, ключ, заголовки и время истечения. Изменить ключ или подставить другой метод нельзя — подпись перестанет совпадать. Именно поэтому presigned URL безопаснее, чем выдача постоянных учётных данных хранилища.
Существенно, что presigned URL — это предъявляемый токен: любой, кто получил ссылку, может ею воспользоваться до истечения срока. Отсюда правила: короткий срок действия, привязка к одному ключу и отсутствие ссылки в логах и истории браузера.
Для более строгого контроля используют POST-политику — форму с условиями, которые хранилище проверяет при загрузке:
{
"expiration": "2026-10-21T15:30:00Z",
"conditions": [
{"bucket": "app-uploads"},
["starts-with", "$key", "uploads/42/"],
["content-length-range", 1, 52428800],
{"content-type": "application/pdf"}
]
}
Условие content-length-range ограничивает размер на стороне хранилища, поэтому клиент не может загрузить файл произвольного объёма, даже если имеет действительную подпись.
Практика: корректная интеграция
1. Срок действия — минуты, не часы. Пять минут достаточно для начала загрузки. Длительный срок превращает ссылку в постоянный доступ при утечке.
2. Ограничивайте размер и тип через политику. Без условия content-length-range подпись разрешает загрузку файла любого объёма, что может исчерпать квоту бакета.
3. Ключ формирует сервер, а не клиент. Если клиент сам задаёт ключ, он может перезаписать чужой объект или записать файл в неожиданное место. Префикс с идентификатором пользователя решает задачу.
4. Настройте CORS. Браузер выполнит загрузку только если бакет разрешает источник и метод PUT. Без этого запрос отклоняется на стороне клиента с неочевидной ошибкой — о механизме см. MDN CORS.
5. Подтверждайте завершение. Приложение не узнаёт о загрузке автоматически. Варианты: явный вызов клиентом после загрузки, событийное уведомление от хранилища или отложенная проверка наличия объекта. Без подтверждения база содержит записи о несуществующих файлах.
6. Не выдавайте URL на чтение публичных объектов. Для публичного контента достаточно обычного URL; подпись нужна только для приватных объектов.
7. Проверяйте содержимое после загрузки. Presigned URL не проверяет, что именно загружено; проверка типа и отсутствия вредоносного содержимого выполняется отдельным шагом — см. статью про безопасность загрузок.
Рекомендации:
- Выдавайте подпись только аутентифицированным пользователям. Проверка прав — задача приложения, хранилище её не выполняет.
- Используйте отдельные учётные данные с минимальными правами. Подпись, созданная ключом с полным доступом, наследует его широту; основы — в статье про MinIO и S3.
- Не логируйте подписанные URL. Ссылка в логе — это действующий токен до истечения срока.
- Обрабатывайте повторную выдачу. Клиент может запросить ссылку дважды; ключ с уникальным суффиксом предотвращает перезапись.
- Проверяйте срок на клиенте. Истёкшая подпись даёт ошибку 403, которую клиент должен обработать повторным запросом, а не повтором загрузки.
Практический сценарий: загрузки падали с 403 в браузере, но работали из curl
Контекст: фронтенд получал ошибку 403 при загрузке файла по presigned URL, тогда как тот же URL из curl работал.
- Диагностика. В ответе хранилища было тело с кодом
AccessDeniedи пустым описанием причины. Заголовки запроса из браузера включалиOrigin. - Проверка CORS. Запрос конфигурации бакета показал, что правила CORS отсутствуют: разрешённые источники не были заданы.
- Уточнение. Хранилище отклоняло предварительный
OPTIONS-запрос, поэтому браузер не отправлялPUTвовсе. Curl не выполняет предварительных запросов, что объясняло расхождение. - Дополнительная причина. Часть ошибок 403 возникала позже: срок ссылки составлял 60 секунд, и на медленных каналах загрузка не успевала начаться.
- Действия. (а) Добавили правило CORS с разрешённым источником, методами
PUTи заголовкамиContent-Type; (б) увеличили срок ссылки до 300 секунд; (в) добавили условиеcontent-length-rangeв политику; (г) ввели повторный запрос подписи на клиенте при 403 с истёкшим сроком; (д) добавили метрику на долю неудачных загрузок и алерт при превышении 5 %. - Результат. Загрузки прошли; ошибка 403 стала различимой по причине — истёкший срок и неверная подпись дают разные сообщения в логе клиента.
Вывод: 403 при загрузке по подписанной ссылке чаще связан с CORS или истёкшим сроком, чем с неверной подписью. Различие между браузером и curl — почти всегда признак предварительного запроса.
FAQ
Чем presigned URL отличается от временных учётных данных? Ссылка разрешает одно действие с одним объектом, тогда как временные учётные данные дают доступ ко всему, что разрешено ролью. Ссылка проще и уже по возможностям.
Можно ли отозвать выданную ссылку до истечения срока? Напрямую — нет, подпись проверяется локально хранилищем. Косвенно помогает смена ключа учётных данных, но это влияет на все выданные ссылки. Поэтому срок делают коротким.
Кто платит за трафик при прямой загрузке? Трафик идёт через хранилище, поэтому приложение разгружается, но расходы на исходящий трафик при последующем чтении остаются. Для публичного контента их снижают через CDN.
Нужен ли presigned URL для чтения файлов внутри сервера? Нет. Сервер использует учётные данные напрямую. Ссылка нужна только внешнему клиенту, у которого этих данных нет.
Источники: AWS S3 docs (presigned URL), MinIO docs, MDN (CORS) — ссылки в шапке.