presigned URL არის ჩვეულებრივი S3 მოთხოვნა, რომლის ხელმოწერაც query string-შია გადატანილი, ასე რომ, ვისაც ეს URL აქვს, შეუძლია ეს ერთი მოთხოვნა შეასრულოს - ჩამოტვირთოს ეს ობიექტი ან ატვირთოს ამ key-ზე - სანამ URL-ს ვადა არ გაუვა, ისე, რომ შენს გასაღებებს ვერასოდეს ხედავს. შენი სერვერი მას რამდენიმე მიკროწამში აგენერირებს, საცავთან დაკავშირების გარეშე, გადასცემს ბრაუზერს ან სკრიპტს და გზიდან იწევა. ეს სწორი ნიმუშია მომხმარებლის ატვირთვებისთვის: ფაილი ბრაუზერიდან პირდაპირ საცავში მიდის და არა შენი აპლიკაციის გავლით, ასე რომ 2 GB-იანი ვიდეო არც worker-ს იკავებს და არც აპლიკაციის სერვერის დისკს ავსებს. ის, რაც ფუჭდება, ყოველთვის ერთი და იგივე ოთხი რამაა: ჰოსტის სახელი, რომლისთვისაც URL მოიწერა, header-ები, რომლებსაც ბრაუზერი აგზავნის და რომლებიც არ მოწერილა, CORS და ვადა. ეს პოსტი ყველა მათგანს განიხილავს, მომუშავე კოდით Python-სა და Node-ში.
როგორ მუშაობს presigned URL#
ჩვეულებრივი S3 მოთხოვნა ხელმოწერას Authorization header-ში ატარებს. presigned მოთხოვნა იმავე ინფორმაციას query პარამეტრებად ატარებს:
https://s3.example.com/media/uploads/3f9c2a.jpg ?X-Amz-Algorithm=AWS4-HMAC-SHA256 &X-Amz-Credential=AKIA...%2F20261008%2Fus-east-1%2Fs3%2Faws4_request &X-Amz-Date=20261008T101500Z &X-Amz-Expires=900 &X-Amz-SignedHeaders=host &X-Amz-Signature=5c1e...ხელმოწერა არის HMAC მეთოდზე, ჰოსტზე, გზაზე, მოწერილ header-ებსა და query პარამეტრებზე, გამოთვლილი შენი საიდუმლო გასაღებით. საცავის სერვერი მას მოთხოვნის მოსვლისას ხელახლა ითვლის. ყველაფერი, რაც ამ შემავალთაგან ერთს ცვლის - სხვა მეთოდი, სხვა ჰოსტის სახელი, დამატებითი მოწერილი header სხვა მნიშვნელობით - სხვა ხელმოწერას იძლევა და 403 SignatureDoesNotMatch-ს.
ამ დიზაინიდან პირდაპირ სამი რამ გამომდინარეობს:
- URL-ის გენერაცია ლოკალურია. SDK შენი საიდუმლოთი არითმეტიკას აკეთებს; სერვერს არაფერს ეკითხება. URL ობიექტისთვის, რომელიც არ არსებობს, უპრობლემოდ გენერირდება და მხოლოდ გამოყენებისას მარცხდება.
- URL არის bearer token. ნებისმიერს, ვისაც ის აქვს, შეუძლია გამოიყენოს, რამდენჯერაც უნდა, სანამ ვადა არ გაუვა. presigned PUT URL ხანმოკლე პაროლივით მოეპყარი.
- access key ID ჩანს,
X-Amz-Credential-ში. საიდუმლო არ ჩანს და URL-იდან ვერ გამოიყვანება. key ID-ის გამჟღავნება თავისთავად ნორმალური და უვნებელია.
ვადა: რამდენ ხანს და რას ნიშნავს#
X-Amz-Expires არის სიცოცხლის ხანგრძლივობა წამებში X-Amz-Date-იდან. Signature Version 4-ით მაქსიმუმი 604,800 წამია - შვიდი დღე. ნაგულისხმევი მნიშვნელობები ხელსაწყოს მიხედვით განსხვავდება:
| ხელსაწყო | ნაგულისხმევი ვადა | როგორ დავაყენოთ |
|---|---|---|
boto3 generate_presigned_url | 3600 s | ExpiresIn= |
JS SDK v3 getSignedUrl | 900 s | { expiresIn: } |
AWS CLI aws s3 presign | 3600 s | --expires-in |
ვადა აირჩიე იმის მიხედვით, რისთვისაა URL. ატვირთვის URL ერთხელ გამოიყენება, გაცემიდან წამებში: ხუთიდან თხუთმეტ წუთამდე სავსებით საკმარისია და ზიანს ზღუდავს, თუ გაჟონავს. ჩამოტვირთვის ბმული, რომელიც ჩაშენებულია გვერდში, რომელიც ყოველ მოთხოვნაზე ირენდერება, შეიძლება მსგავსი იყოს. ელფოსტით გაგზავნილმა ბმულმა უნდა იცოცხლოს, სანამ მიმღები გახსნის, რაც დღეებს გულისხმობს - მაგრამ ყოველი დღე არის დღე, როცა ბმული მუშაობს ნებისმიერისთვის, ვისაც გადაუგზავნიან.
ვადა მოწმდება მაშინ, როცა მოთხოვნა იწყება, და არა მაშინ, როცა მთავრდება. AWS-ზე ატვირთვას, რომელიც ვადის გასვლამდე ერთი წამით ადრე დაიწყო, დასრულების უფლება აქვს, თუნდაც ერთი საათი დასჭირდეს; S3-თავსებადი სერვერები ზოგადად ასევე იქცევიან. დროის შემოწმება იმასაც ნიშნავს, რომ საათებს მნიშვნელობა აქვს: URL, რომელიც დაგენერირდა მანქანაზე, რომლის საათიც ათი წუთით წინ უსწრებს, ათი წუთის განმავლობაში "ჯერ არ არის მოქმედი". ჰოსტინგის სერვერები დროს ზუსტად ინახავს; ლეპტოპები და კონტეინერები NTP-ის გარეშე ზოგჯერ არა.
URL-ების გენერაცია Python-სა და Node-ში#
კლიენტი მორგებული endpoint-ისთვის ზუსტად ისე უნდა იყოს კონფიგურირებული, როგორც ჩვეულებრივი მოთხოვნებისთვის: endpoint, რეგიონი, path-style. S3 Node.js-იდან და Python-იდან კლიენტის სრულ დაყენებას შეიცავს; presign-ის გამოძახებები ამას რამდენიმე ხაზს ამატებს.
# Download link valid for 10 minutesurl = s3.generate_presigned_url( "get_object", Params={"Bucket": "media", "Key": "reports/2026/q3.pdf", "ResponseContentDisposition": 'attachment; filename="q3.pdf"'}, ExpiresIn=600,)# Upload URL for one key, valid for 5 minutesput_url = s3.generate_presigned_url( "put_object", Params={"Bucket": "media", "Key": "uploads/3f9c2a.jpg", "ContentType": "image/jpeg"}, ExpiresIn=300,)import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";import { getSignedUrl } from "@aws-sdk/s3-request-presigner";const getUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket: "media", Key: "reports/2026/q3.pdf" }), { expiresIn: 600 });const putUrl = await getSignedUrl(s3, new PutObjectCommand({ Bucket: "media", Key: "uploads/3f9c2a.jpg", ContentType: "image/jpeg" }), { expiresIn: 300 });shell-იდან aws s3 presign s3://media/reports/2026/q3.pdf --expires-in 600 აგენერირებს GET URL-ს იმავე profile-ის პარამეტრებით, რითაც CLI-ის ყველა სხვა ბრძანება. CLI მხოლოდ ჩამოტვირთვებს აწერს ხელს; ატვირთვის URL-ისთვის SDK გჭირდება.
ResponseContentDisposition-ისა და მისი მონათესავეების (ResponseContentType, ResponseCacheControl) ცოდნა ღირს. ისინი GET URL-ში მოიწერება და სერვერს ეუბნება, პასუხში ეს header გადაფაროს - ასე რომ ერთი და იგივე შენახული ობიექტი ერთ გვერდზე შეიძლება ჩაშენებულ სურათად შეთავაზდეს, მეორეზე კი ჩამოსატვირთად, მოსახერხებელი ფაილის სახელით.
ჰოსტის სახელი ის უნდა იყოს, რომელსაც ბრაუზერი იყენებს#
რადგან ჰოსტი მოწერილია, URL-ები დააგენერირე კლიენტით, რომლის endpoint-იც ის საჯარო მისამართია, რომელსაც ბრაუზერი მიმართავს. ეს იჭერს მათ, ვისაც აპლიკაცია და საცავი ერთ ქსელში აქვთ გაშვებული:
- აპლიკაცია საცავს შიდა მისამართით ან პირდაპირი plain-HTTP პორტით ესაუბრება და URL-ებს ამ კლიენტით აგენერირებს.
- ბრაუზერი იღებს
http://203.0.113.10:PORT/...-ს, რომელიც ან მიუწვდომელია, ან HTTPS გვერდზე mixed content-ად იბლოკება, ან ორივე. - URL-ში ჰოსტის ხელით შეცვლა ხელმოწერას აუქმებს.
თუ აპლიკაციას საკუთარი ტრაფიკისთვის მართლა სხვა მისამართი სჭირდება, შექმენი ორი კლიენტი: ერთი აპლიკაციის ოპერაციებისთვის, მეორე საჯარო HTTPS endpoint-ით, რომელიც მხოლოდ presign-ისთვის გამოიყენება. რადგან URL-ის გენერაცია სერვერს არასოდეს უკავშირდება, მეორე კლიენტს საცავამდე მიღწევაც კი არ სჭირდება.
RE:NODE-ის S3 საცავზე საჯარო მისამართი არის ჰოსტის სახელი, რომელსაც გეგმის proxy სლოტზე მიმართავ და რომელიც HTTPS-ს ემსახურება სერტიფიკატით, რომელიც შენთვის გაიცემა და ახლდება. მოაწერე URL-ებს https://s3.example.com-ისთვის და ბრაუზერი, სერტიფიკატი და ხელმოწერა ერთმანეთს დაეთანხმება.
ატვირთვა ბრაუზერიდან: სრული ნაკადი#
ნიმუშს ოთხი ნაბიჯი აქვს, და მესამე ისაა, რომელიც შენს bucket-ს სუფთად ინახავს.
- ბრაუზერი შენს აპლიკაციას ატვირთვის URL-ს სთხოვს და უგზავნის ფაილის სახელს, ზომას და ტიპს. აპლიკაცია ამოწმებს, აქვს თუ არა მომხმარებელს ატვირთვის უფლება, ადარებს გამოცხადებულ ზომასა და ტიპს თავის ლიმიტებს და key-ს თავად აგენერირებს - შემთხვევითი ID გაფართოებით, არასოდეს მომხმარებლის ფაილის სახელი.
- აპლიკაცია აბრუნებს key-ს და მისთვის presigned PUT URL-ს, მოწერილს გამოცხადებული
ContentType-ით. - ბრაუზერი ფაილს URL-ზე PUT-ით აგზავნის ზუსტად იმ
Content-Typeheader-ით. - ბრაუზერი აპლიკაციას ეუბნება, რომ ატვირთვა დასრულდა. აპლიკაცია key-ზე
HEADმოთხოვნას აგზავნის, რომ დაადასტუროს, რომ ობიექტი არსებობს და მისი ზომა და ტიპი გამოცხადებულს ემთხვევა, შემდეგ კი მონაცემთა ბაზაში წერს. ყველაფერი, რაც არასოდეს დადასტურდა, მოგვიანებით შეიძლება გასუფთავდეს: ჩამოთვალე ატვირთვის prefix და წაშალე ერთ დღეზე ძველი key-ები, რომლებსაც ბაზაში ჩანაწერი არ აქვთ.
ბრაუზერის მხარე უბრალო fetch-ია:
const { key, url } = await fetch("/api/uploads", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: file.name, size: file.size, type: file.type }),}).then((r) => r.json());const put = await fetch(url, { method: "PUT", headers: { "Content-Type": file.type }, body: file });if (!put.ok) throw new Error(`upload failed: ${put.status}`);await fetch("/api/uploads/confirm", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key }),});თუ აპლიკაციამ ContentType: "image/jpeg" მოაწერა, ბრაუზერი კი image/png-ს აგზავნის ან არაფერს, ხელმოწერა მარცხდება. მოაწერე ზუსტად ის ტიპი, რომელსაც გაგზავნი, და ის გაგზავნე.
fetch ატვირთვის პროგრესს არ გაძლევს. პროგრესის ზოლისთვის გამოიყენე XMLHttpRequest და მისი upload.onprogress მოვლენა; დანარჩენში მოთხოვნა იდენტურია.
CORS: რატომ მუშაობს ატვირთვა curl-ში და არა ბრაუზერში#
გვერდი https://app.example.com-ზე, რომელიც PUT-ს https://s3.example.com-ზე აგზავნის, cross-origin მოთხოვნაა. მის გაგზავნამდე ბრაუზერი preflight OPTIONS მოთხოვნას აკეთებს, და საცავის სერვერმა უნდა უპასუხოს header-ებით, რომლებიც ამ origin-ს, ამ მეთოდს და Content-Type header-ს უშვებს. თუ არ უპასუხა, ბრაუზერი ატვირთვას ბლოკავს და კონსოლში CORS-ის შეცდომა ჩანს - მაშინ, როცა იგივე URL curl-იდან იდეალურად მუშაობს, რადგან curl CORS-ს არ აკეთებს.
S3-ზე პასუხი არის CORS-ის კონფიგურაცია bucket-ზე:
{ "CORSRules": [ { "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["GET", "PUT", "HEAD"], "AllowedHeaders": ["Content-Type"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3600 } ]}$ aws s3api put-bucket-cors --bucket media --cors-configuration file://cors.json --profile store$ aws s3api get-bucket-cors --bucket media --profile store$ curl -i -X OPTIONS https://s3.example.com/media/test \ -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: PUT"S3-თავსებადი სერვერები განსხვავდება იმით, bucket-ის CORS API-ის რა ნაწილს ახორციელებენ, ამიტომ სანამ მასზე რამეს ააშენებ, გამოსცადე: დააყენე წესი, შემდეგ curl-ით გაგზავნე preflight და პასუხში მოძებნე Access-Control-Allow-Origin. თუ bucket-ს CORS-ის წესს ვერ მიანიჭებ, სათადარიგო გზაა ატვირთვა შენი აპლიკაციის გავლით (აპლიკაცია იღებს ფაილს და SDK-ით საცავში წერს), რაც აპლიკაციის გამტარუნარიანობა და მეხსიერება ჯდება, მაგრამ CORS საერთოდ არ სჭირდება. bucket-ზე, რომელშიც ტვირთავ, AllowedOrigins: ["*"]-ს ნუ მიმართავ; ჩამოთვალე origin-ები, რომლებსაც მართლა ემსახურები.
ზომის ლიმიტები და POST პოლიტიკები#
presigned PUT თავისით მაქსიმალურ ზომას ვერ აიძულებს. აპლიკაციას შეუძლია უარი თქვას URL-ის გაცემაზე გამოცხადებული 5 GB-იანი ფაილისთვის, მაგრამ არაფერი უშლის ბრაუზერს, 5 MB გამოაცხადოს და 5 GB გამოაგზავნოს. სწორედ ამიტომ მე-4 ნაბიჯი რეალურ ზომას HEAD-ით ამოწმებს და შლის ყველაფერს, რაც ლიმიტს აჭარბებს.
S3 API-ს აქვს მეორე მექანიზმი, ბრაუზერის ფორმებისთვის შექმნილი: presigned POST. URL-ის ნაცვლად სერვერი გასცემს პოლიტიკის დოკუმენტს, რომელიც პირობებს ჩამოთვლის - key ან key-ის prefix, შიგთავსის ტიპი და content-length-range - ბრაუზერი კი bucket-ის URL-ზე multipart ფორმას აგზავნის, რომელშიც პოლიტიკა და მისი ხელმოწერა ველებადაა. სერვერი უარყოფს ატვირთვებს, რომლებიც რომელიმე პირობას არღვევს.
post = s3.generate_presigned_post( Bucket="media", Key="uploads/3f9c2a.jpg", Fields={"Content-Type": "image/jpeg"}, Conditions=[{"Content-Type": "image/jpeg"}, ["content-length-range", 1, 10 * 1024 * 1024]], ExpiresIn=300,)# post["url"] and post["fields"] go to the browser as a formNode-ში ეკვივალენტია createPresignedPost @aws-sdk/s3-presigned-post-იდან. POST პოლიტიკის მხარდაჭერა S3-თავსებად სერვერებს შორის უფრო მეტად განსხვავდება, ვიდრე PUT-ისა, ამიტომ იგივე წესი მოქმედებს: ლიმიტი გამოსცადე განზრახ ზედმეტად დიდი ფაილის ატვირთვით და დარწმუნდი, რომ უარყოფილია. თუ ლიმიტი არ სრულდება, HEAD-ის შემოწმება დატოვე.
რამდენიმე ასეულ მეგაბაიტზე დიდი ფაილებისთვის ერთი PUT მყიფეა - ერთი გაწყვეტილი კავშირი ყველაფერს თავიდან იწყებს, და ერთი PUT ისედაც 5 GB-ით შემოიფარგლება. S3-ის პასუხი არის presigned multipart ატვირთვა: აპლიკაცია იძახებს CreateMultipartUpload-ს, თითოეულ ნაწილზე ერთ UploadPart URL-ს აწერს ხელს, ბრაუზერი ნაწილებს ტვირთავს (წარუმატებლებს თავიდან ცდის), აპლიკაცია კი იძახებს CompleteMultipartUpload-ს ნაწილების ETag-ებით. ბრაუზერის ნახევარს ახორციელებს ბიბლიოთეკები, მაგალითად Uppy-ის S3 plugin. აქ მეტი მოძრავი ნაწილია, ამიტომ გამოიყენე მხოლოდ მაშინ, როცა ფაილები მართლა დიდია.
უსაფრთხოების საკონტროლო სია#
- key-ები სერვერზე დააგენერირე. ბრაუზერს key-ის არჩევის უფლება არასოდეს მისცე; მას შეუძლია სხვა მომხმარებლის ფაილი გადააწეროს ან თავისი prefix-ის გარეთ ჩაწეროს.
- ავტორიზაცია ხელმოწერამდე. endpoint, რომელიც ატვირთვის URL-ებს გასცემს, არის კარიბჭე. შეუზღუდე მას სიხშირე თითოეულ მომხმარებელზე, რომ მისით შენი საცავის შევსება ვერ მოხერხდეს.
- მოკლე ვადები PUT-ისთვის. ხუთიდან თხუთმეტ წუთამდე.
- ატვირთვის შემდეგ შეამოწმე. ზომა, ტიპი და - სურათებისთვის - რომ ფაილი მართლა სურათად იშიფრება, სანამ მას სხვას აჩვენებ.
- მომხმარებლის ატვირთვები ფრთხილად მიაწოდე. მომხმარებლის ატვირთული HTML ან SVG ფაილი, რომელიც შენს კონტროლქვეშ მყოფი დომენიდან ჩაშენებულად მიეწოდება, ვიზიტორების ბრაუზერებში სკრიპტს გაუშვებს. ასეთი ფაილები მიაწოდე
Content-Disposition: attachment-ით ან შენი აპლიკაციისგან ცალკე ჰოსტის სახელიდან. - საიდუმლო კლიენტს არ მიაწოდო. ბრაუზერი URL-ებს იღებს, გასაღებებს არასოდეს. საიდუმლოები აპლიკაციის გარემოში ცხოვრობს - იხილე გარემოს ცვლადები და საიდუმლოები.
კერძო ჩამოტვირთვები იმავე ლოგიკას მიჰყვება, საპირისპიროდ: ობიექტები კერძო დატოვე, უფლებები შენს აპლიკაციაში შეამოწმე, როცა მომხმარებელი ფაილს ითხოვს, და უპასუხე ხანმოკლე presigned GET-ით ან მასზე გადამისამართებით. ასე საცავს საჯაროობა არასოდეს სჭირდება და წვდომის კონტროლი ერთ ადგილას რჩება - შენს კოდში.
@app.get("/files/<int:file_id>")def download(file_id): f = File.query.get_or_404(file_id) if f.owner_id != current_user.id: abort(403) url = s3.generate_presigned_url( "get_object", Params={"Bucket": BUCKET, "Key": f.key, "ResponseContentDisposition": f'attachment; filename="{f.safe_name}"'}, ExpiresIn=120, ) return redirect(url, code=302)შენს გვერდზე ბმული შენს საკუთარ route-ზე მიუთითებს, რომელსაც ვადა არასოდეს გასდის და რომელსაც შენი ლოგინი იცავს; presigned URL გადამისამართების უკან ორ წუთს ცოცხლობს და არავის არასოდეს ეჩვენება. ბრაუზერები გადამისამართებას მომხმარებლისთვის შეუმჩნევლად მიჰყვებიან, მისამართის ზოლიდან დაკოპირებული ბმული კი თითქმის მაშინვე წყვეტს მუშაობას. ამ გადამისამართების პასუხს cache ნუ გაუკეთებ, თორემ CDN-მა ან proxy-მ შეიძლება ერთი მომხმარებლის URL მეორეს მისცეს - გაგზავნე მასთან ერთად Cache-Control: no-store.
FAQ#
შეიძლება presigned URL-ის ერთზე მეტჯერ გამოყენება?
კი. ის მოქმედებს ნებისმიერი რაოდენობის მოთხოვნისთვის, სანამ ვადა არ გაუვა. ორჯერ გამოყენებული PUT URL უბრალოდ გადააწერს ობიექტს მეორე ატვირთვით. თუ ერთჯერადი გამოყენება გჭირდება, გაცემული key-ები შენს მონაცემთა ბაზაში აღრიცხე და ერთი და იმავე key-ის ორჯერ ჩაწერაზე უარი თქვი.
რატომ აბრუნებს ჩემი presigned URL SignatureDoesNotMatch-ს?
მოთხოვნა განსხვავდება იმისგან, რაც მოიწერა. ჩვეულებრივი მიზეზებია ჰოსტის სახელი, რომელიც განსხვავდება იმისგან, რითაც კლიენტი იყო კონფიგურირებული, Content-Type header, რომელიც მოწერილს არ ემთხვევა, ან proxy, რომელიც Host header-ს გადაწერს. URL დააგენერირე საჯარო endpoint-ით და გაგზავნე ზუსტად მოწერილი header-ები.
მაქსიმუმ რამდენ ხანს შეიძლება იცოცხლოს presigned URL-მა?
შვიდი დღე (604,800 წამი) Signature Version 4-ით. უფრო ხანგრძლივი საჭიროებისთვის ახალი URL დააგენერირე, როცა მომხმარებელი ფაილს ითხოვს, ნაცვლად იმისა, რომ URL-ები შეინახო.
მუშაობს presigned URL-ები path-style endpoint-ებთან?
კი. URL-ს უბრალოდ bucket გზაში აქვს - https://s3.example.com/bucket/key?.... კლიენტი ხელმოწერისას path-style-ზე უნდა იყოს კონფიგურირებული, როგორც ახსნილია სტატიაში path-style vs virtual-hosted URL-ები.
იქნებ ამის ნაცვლად bucket საჯარო გავხადო?
კერძო მომხმარებლის ფაილებისთვის - არა: presigned URL-ები წვდომის კონტროლს შენს აპლიკაციაში ინახავს. საჯარო წაკითხვა მართლა საჯარო რესურსებს უხდება, მაგრამ ის დამოკიდებულია იმაზე, რას უჭერს მხარს საცავის პროვაიდერი, ამიტომ შეამოწმე, სანამ მასზე დიზაინს ააგებ.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.