OSS bucket objects are private by default — only the object owner can access them. Presigned URLs let you delegate temporary upload access to third parties without sharing your credentials. Generate a presigned PUT URL with the Go SDK V2, share it, and anyone with the URL can upload an object until it expires. The URL can be reused multiple times before expiry.
Prerequisites
Before you begin, ensure that you have:
An OSS bucket
Access credentials with the
oss:PutObjectpermission on the target bucket (required for the user who generates the presigned URL; generating the presigned URL itself requires no special permissions for the uploader)The OSS Go SDK V2 installed:
github.com/aliyun/alibabacloud-oss-go-sdk-v2
How it works
Presigning separates the party that authorizes uploads from the party that performs them:
The object owner (your server) calls
client.Presign()with aPutObjectRequestand an expiry duration. The SDK signs the request using Signature V4 and embeds the signature in the URL's query parameters.The owner shares the presigned URL with a third party.
The third party sends an HTTP PUT request directly to OSS using the URL — no OSS credentials required.
OSS verifies the embedded signature. If valid and the URL has not expired, OSS accepts the upload.
Treat presigned URLs as bearer tokens. Anyone who has the URL can upload an object until it expires. Share URLs only with intended recipients and use short expiry durations for sensitive operations.
The diagram below shows the full flow:
The Presign method
func (c *Client) Presign(ctx context.Context, request any, optFns ...func(*PresignOptions)) (result *PresignResult, err error)Return values
Presign returns a *PresignResult:
| Field | Type | Description |
|---|---|---|
Method | string | HTTP method for the operation (PUT for PutObjectRequest) |
URL | string | The presigned URL |
Expiration | time.Time | When the URL expires |
SignedHeaders | map[string]string | Request headers that are part of the signature. The uploader must send these exact headers when using the URL. |
Generate a presigned URL
The code sample below uses the cn-hangzhou region and a public endpoint. For access from other Alibaba Cloud services in the same region, use an internal endpoint instead. See Regions and endpoints.
Access credentials are read from environment variables. See Configure access credentials.
package main
import (
"context"
"flag"
"log"
"time"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)
var (
region string
bucketName string
objectName string
)
func init() {
flag.StringVar(®ion, "region", "", "The region in which the bucket is located.")
flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
flag.StringVar(&objectName, "object", "", "The name of the object.")
}
func main() {
flag.Parse()
if len(bucketName) == 0 {
flag.PrintDefaults()
log.Fatalf("invalid parameters, bucket name required")
}
if len(region) == 0 {
flag.PrintDefaults()
log.Fatalf("invalid parameters, region required")
}
if len(objectName) == 0 {
flag.PrintDefaults()
log.Fatalf("invalid parameters, object name required")
}
// Load credentials from environment variables and configure the region.
cfg := oss.LoadDefaultConfig().
WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
WithRegion(region)
client := oss.NewClient(cfg)
// Generate a presigned PUT URL that expires in 10 minutes.
result, err := client.Presign(context.TODO(), &oss.PutObjectRequest{
Bucket: oss.Ptr(bucketName),
Key: oss.Ptr(objectName),
},
oss.PresignExpires(10*time.Minute),
)
if err != nil {
log.Fatalf("failed to presign PutObject request: %v", err)
}
log.Printf("Method: %v\n", result.Method)
log.Printf("Expiration: %v\n", result.Expiration)
log.Printf("URL: %v\n", result.URL)
// If SignedHeaders is non-empty, the uploader must include these headers
// in the PUT request. Omitting them causes a signature error.
if len(result.SignedHeaders) > 0 {
log.Printf("Signed headers (must be sent by the uploader):\n")
for k, v := range result.SignedHeaders {
log.Printf(" %v: %v\n", k, v)
}
}
}Presigned URL structure
The generated URL embeds the signature and expiry in its query parameters:
https://exampleobject.oss-cn-hangzhou.aliyuncs.com/exampleobject.txt
?x-oss-date=20241112T083238Z
&x-oss-expires=3599
&x-oss-signature-version=OSS4-HMAC-SHA256
&x-oss-credential=LTAI***%2F20241112%2Fcn-hangzhou%2Foss%2Faliyun_v4_request
&x-oss-signature=ed5a******| Query parameter | Description |
|---|---|
x-oss-date | Signing timestamp in ISO 8601 format |
x-oss-expires | TTL in seconds, measured from x-oss-date |
x-oss-signature-version | Signature algorithm (OSS4-HMAC-SHA256 for Signature V4) |
x-oss-credential | Credential scope used to compute the signature |
x-oss-signature | The computed signature |
Share this URL with the uploader. The URL is valid for the duration you specified.
Upload using the presigned URL
The uploader sends an HTTP PUT request with the file as the request body. All examples use <signedUrl> as a placeholder — replace it with the actual URL from the previous step.
If SignedHeaders was non-empty when you generated the URL, the uploader must include those exact headers. Sending different headers causes OSS to return a SignatureDoesNotMatch error.
curl
curl -X PUT -T /path/to/local/file "<signedUrl>"Go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func uploadFile(signedUrl, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("unable to open file: %w", err)
}
defer file.Close()
client := &http.Client{}
req, err := http.NewRequest("PUT", signedUrl, file)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
fmt.Printf("Status: %d\n", resp.StatusCode)
fmt.Println(string(body))
return nil
}
func main() {
signedUrl := "<signedUrl>"
filePath := "/path/to/local/file"
if err := uploadFile(signedUrl, filePath); err != nil {
fmt.Println("Upload error:", err)
}
}Java
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
public class SignUrlUpload {
public static void main(String[] args) throws Throwable {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
URL signedUrl = new URL("<signedUrl>");
String pathName = "/path/to/local/file";
try {
HttpPut put = new HttpPut(signedUrl.toString());
HttpEntity entity = new FileEntity(new File(pathName));
put.setEntity(entity);
httpClient = HttpClients.createDefault();
response = httpClient.execute(put);
System.out.println("Status: " + response.getStatusLine().getStatusCode());
} catch (Exception e) {
e.printStackTrace();
} finally {
response.close();
httpClient.close();
}
}
}Python
import requests
def upload_file(signed_url, file_path):
try:
with open(file_path, 'rb') as file:
response = requests.put(signed_url, data=file)
print(f"Status: {response.status_code}")
print(response.text)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
signed_url = "<signedUrl>"
file_path = "/path/to/local/file"
upload_file(signed_url, file_path)Node.js
const fs = require('fs');
const axios = require('axios');
async function uploadFile(signedUrl, filePath) {
try {
const fileStream = fs.createReadStream(filePath);
const response = await axios.put(signedUrl, fileStream, {
headers: { 'Content-Type': 'application/octet-stream' }
});
console.log(`Status: ${response.status}`);
} catch (error) {
console.error(`Error: ${error.message}`);
}
}
(async () => {
const signedUrl = '<signedUrl>';
const filePath = '/path/to/local/file';
await uploadFile(signedUrl, filePath);
})();Browser.js
Browsers automatically add a Content-Type header to PUT requests. If Content-Type was not included when you generated the presigned URL, the signature will not match and OSS returns a 403 SignatureDoesNotMatch error. To avoid this, always specify ContentType in PutObjectRequest when generating URLs for browser-based uploads.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Upload</title>
</head>
<body>
<input type="file" id="fileInput" />
<button id="uploadButton">Upload</button>
<script>
const signedUrl = "<signedUrl>";
document.getElementById('uploadButton').addEventListener('click', async () => {
const file = document.getElementById('fileInput').files[0];
if (!file) {
alert('Select a file to upload.');
return;
}
try {
const response = await fetch(signedUrl, {
method: 'PUT',
body: file,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
alert('Uploaded successfully.');
} catch (error) {
console.error('Error:', error);
alert('Upload failed: ' + error.message);
}
});
</script>
</body>
</html>C#
using System.Net.Http.Headers;
var filePath = "/path/to/local/file";
var presignedUrl = "<signedUrl>";
using var httpClient = new HttpClient();
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);
var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Upload successful. Status: {response.StatusCode}");
}
else
{
string responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Upload failed. Status: {response.StatusCode}");
Console.WriteLine("Response: " + responseContent);
}C++
#include <iostream>
#include <fstream>
#include <curl/curl.h>
void uploadFile(const std::string& signedUrl, const std::string& filePath) {
CURL* curl = curl_easy_init();
if (!curl) return;
curl_global_init(CURL_GLOBAL_DEFAULT);
FILE* file = fopen(filePath.c_str(), "rb");
if (!file) {
std::cerr << "Unable to open file: " << filePath << std::endl;
return;
}
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str());
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);
curl_easy_setopt(curl, CURLOPT_READDATA, file);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "curl failed: " << curl_easy_strerror(res) << std::endl;
} else {
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
std::cout << "Status: " << httpCode << std::endl;
}
fclose(file);
curl_easy_cleanup(curl);
curl_global_cleanup();
}
int main() {
std::string signedUrl = "<signedUrl>";
std::string filePath = "/path/to/local/file";
uploadFile(signedUrl, filePath);
return 0;
}Android
package com.example.signurlupload;
import android.os.AsyncTask;
import android.util.Log;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class SignUrlUploadActivity {
private static final String TAG = "SignUrlUploadActivity";
public void uploadFile(String signedUrl, String filePath) {
new UploadTask().execute(signedUrl, filePath);
}
private class UploadTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
String signedUrl = params[0];
String filePath = params[1];
HttpURLConnection connection = null;
DataOutputStream dos = null;
FileInputStream fis = null;
try {
URL url = new URL(signedUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("PUT");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/octet-stream");
fis = new FileInputStream(filePath);
dos = new DataOutputStream(connection.getOutputStream());
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
dos.write(buffer, 0, length);
}
dos.flush();
int responseCode = connection.getResponseCode();
Log.d(TAG, "Status: " + responseCode);
return "Upload complete. Status: " + responseCode;
} catch (IOException e) {
e.printStackTrace();
return "Upload failed: " + e.getMessage();
} finally {
if (connection != null) connection.disconnect();
}
}
@Override
protected void onPostExecute(String result) {
Log.d(TAG, result);
}
}
public static void main(String[] args) {
SignUrlUploadActivity activity = new SignUrlUploadActivity();
String signedUrl = "<signedUrl>";
String filePath = "/path/to/local/file";
activity.uploadFile(signedUrl, filePath);
}
}Best practices
Set short expiry durations. The maximum TTL for Signature V4 is 7 days, but use the shortest duration that works for your use case. This limits the exposure window if a URL is intercepted.
Always specify `Content-Type` for browser uploads. Browsers automatically add a Content-Type header to PUT requests. Include ContentType in PutObjectRequest when generating the presigned URL so the signatures match.
Match signed headers exactly. Any header included in PutObjectRequest at presigning time — such as Content-Type, x-oss-storage-class, or user metadata — becomes part of the signature. The uploader must send those exact headers with the same values. Check result.SignedHeaders to see which headers to pass.
Generate one URL per upload session. Do not cache presigned URLs across user sessions or requests with different parameters. Generate a fresh URL each time to avoid reuse and signature mismatches.
Common scenarios
Troubleshooting
SignatureDoesNotMatch (403)
This error means OSS rejected the signature. Check the following in order:
Signed headers not sent by the uploader. Check
result.SignedHeaders— any header listed there must be sent with the exact same value in the PUT request. This is the most common cause.Content-Type mismatch. If
ContentTypewas set when generating the URL, the uploader must send the sameContent-Typeheader. Browsers addContent-Typeautomatically, which can cause a mismatch if it was not included at presign time.URL modified after generation. Use the URL exactly as returned. Do not encode, decode, or alter the query string.
URL expired. Check
result.Expiration. If the URL has expired, generate a new one.System clock skew. Signature V4 is time-sensitive. Make sure your server clock is synchronized (for example, using NTP). Excessive clock skew causes signature validation to fail.
Wrong region. The URL is region-specific. Confirm that the region in your client configuration matches the bucket's actual region.
URL works from curl but not from a browser
Browsers automatically add headers (especially Content-Type) that were not present when the URL was generated. Regenerate the presigned URL with ContentType explicitly set to the value the browser will send.
Upload succeeds but callback is not triggered
Verify the callback URL is publicly reachable from the internet.
Check that the Base64-encoded JSON is correctly formed — decode it to confirm the structure before embedding.