默认情况下,OSS Bucket中的文件是私有的,仅文件拥有者可访问。本文介绍如何使用OSS Go SDK生成带有过期时间的PUT方法预签名URL,以允许他人临时上传文件。在有效期内可多次访问,超期后需重新生成。
注意事项
本文示例代码以华东1(杭州)的地域ID
cn-hangzhou
为例,默认使用外网Endpoint,如果您希望通过与OSS同地域的其他阿里云产品访问OSS,请使用内网Endpoint。关于OSS支持的Region与Endpoint的对应关系,请参见OSS地域和访问域名。本文以从环境变量读取访问凭证为例。如何配置访问凭证,请参见配置访问凭证。
预签名URL无需权限即可生成,但仅当您拥有
oss:PutObject
权限时,第三方才能通过该预签名URL成功上传文件。具体授权操作,请参见为RAM用户授权自定义的权限策略。本文示例代码使用V4预签名URL,有效期最大为7天。更多信息,请参见签名版本4(推荐)。
使用过程
使用PUT方式的预签名URL上传文件的过程如下:
方法定义
您可以使用预签名接口生成预签名URL,授予对存储空间中对象的限时访问权限。在过期时间之前,您可以多次使用预签名URL。
预签名接口定义如下:
func (c *Client) Presign(ctx context.Context, request any, optFns ...func(*PresignOptions)) (result *PresignResult, err error)
请求参数列表
参数名 | 类型 | 说明 |
ctx | context.Context | 请求的上下文 |
request | *PutObjectRequest | 设置需要生成预签名URL的接口名 |
optFns | ...func(*PresignOptions) | (可选)设置过期时间,如果不指定,默认有效期为15分钟 |
其中,PresignOptions选项列举如下:
选项值 | 类型 | 说明 |
Expires | time.Duration | 从当前时间开始,多长时间过期。例如设置一个有效期为30分钟,30 * time.Minute |
Expiration | time.Time | 绝对过期时间 |
在签名版本V4下,有效期最长为7天。同时设置Expiration和Expires时,优先取Expiration。
返回值列表
返回值名 | 类型 | 说明 |
result | *PresignResult | 返回结果,包含预签名URL,HTTP方法,过期时间和参与签名的请求头 |
err | error | 请求的状态,当请求失败时,err不为nil |
其中,PresignResult返回值列举如下:
参数名 | 类型 | 说明 |
Method | string | HTTP方法,和接口对应,例如PutObject接口,返回PUT |
URL | string | 预签名URL |
Expiration | time.Time | 预签名URL的过期时间 |
SignedHeaders | map[string]string | 被签名的请求头,例如PutObject接口,设置了Content-Type时,会返回 Content-Type的信息 |
示例代码
文件拥有者生成PUT方法的预签名URL。
重要在生成PUT方法的预签名URL时,如果指定了请求头,确保在通过该预签名URL发起PUT请求时也包含相应的请求头,以免出现不一致,导致请求失败和签名错误。
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 // 对象名称 ) // init函数用于初始化命令行参数 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() // 检查bucket名称是否为空 if len(bucketName) == 0 { flag.PrintDefaults() log.Fatalf("invalid parameters, bucket name required") } // 检查region是否为空 if len(region) == 0 { flag.PrintDefaults() log.Fatalf("invalid parameters, region required") } // 检查object名称是否为空 if len(objectName) == 0 { flag.PrintDefaults() log.Fatalf("invalid parameters, object name required") } // 加载默认配置并设置凭证提供者和区域 cfg := oss.LoadDefaultConfig(). WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()). WithRegion(region) // 创建OSS客户端 client := oss.NewClient(cfg) // 生成PutObject的预签名URL 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 put object presign %v", err) } log.Printf("request method:%v\n", result.Method) log.Printf("request expiration:%v\n", result.Expiration) log.Printf("request url:%v\n", result.URL) if len(result.SignedHeaders) > 0 { //当返回结果包含签名头时,使用签名URL发送Put请求时,需要设置相应的请求头 log.Printf("signed headers:\n") for k, v := range result.SignedHeaders { log.Printf("%v: %v\n", k, v) } } }
其他人使用PUT方法的预签名URL上传文件。
curl
curl -X PUT -T /path/to/local/file "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******************************************************"
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; import java.util.*; public class SignUrlUpload { public static void main(String[] args) throws Throwable { CloseableHttpClient httpClient = null; CloseableHttpResponse response = null; // 将<signedUrl>替换为授权URL。 URL signedUrl = new URL("<signedUrl>"); // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 String pathName = "C:\\Users\\demo.txt"; try { HttpPut put = new HttpPut(signedUrl.toString()); System.out.println(put); HttpEntity entity = new FileEntity(new File(pathName)); put.setEntity(entity); httpClient = HttpClients.createDefault(); response = httpClient.execute(put); System.out.println("返回上传状态码:"+response.getStatusLine().getStatusCode()); if(response.getStatusLine().getStatusCode() == 200){ System.out.println("使用网络库上传成功"); } System.out.println(response.toString()); } catch (Exception e){ e.printStackTrace(); } finally { response.close(); httpClient.close(); } } }
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("无法打开文件: %w", err) } defer file.Close() // 创建一个新的HTTP客户端 client := &http.Client{} // 创建一个PUT请求 req, err := http.NewRequest("PUT", signedUrl, file) if err != nil { return fmt.Errorf("创建请求失败: %w", err) } // 发送请求 resp, err := client.Do(req) if err != nil { return fmt.Errorf("发送请求失败: %w", err) } defer resp.Body.Close() // 读取响应 body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("读取响应失败: %w", err) } fmt.Printf("返回上传状态码: %d\n", resp.StatusCode) if resp.StatusCode == 200 { fmt.Println("使用网络库上传成功") } fmt.Println(string(body)) return nil } func main() { // 将<signedUrl>替换为授权URL。 signedUrl := "<signedUrl>" // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 filePath := "C:\\Users\\demo.txt" err := uploadFile(signedUrl, filePath) if err != nil { fmt.Println("发生错误:", err) } }
python
import requests def upload_file(signed_url, file_path): try: # 打开文件 with open(file_path, 'rb') as file: # 发送PUT请求上传文件 response = requests.put(signed_url, data=file) print(f"返回上传状态码:{response.status_code}") if response.status_code == 200: print("使用网络库上传成功") print(response.text) except Exception as e: print(f"发生错误:{e}") if __name__ == "__main__": # 将<signedUrl>替换为授权URL。 signed_url = "<signedUrl>" # 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 file_path = "C:\\Users\\demo.txt" 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); // 发送PUT请求上传文件 const response = await axios.put(signedUrl, fileStream, { headers: { 'Content-Type': 'application/octet-stream' // 根据实际情况调整Content-Type } }); console.log(`返回上传状态码:${response.status}`); if (response.status === 200) { console.log('使用网络库上传成功'); } console.log(response.data); } catch (error) { console.error(`发生错误:${error.message}`); } } // 主函数 (async () => { // 将<signedUrl>替换为授权URL。 const signedUrl = '<signedUrl>'; // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 const filePath = 'C:\\Users\\demo.txt'; await uploadFile(signedUrl, filePath); })();
browser.js
重要如果您使用 Browser.js 上传文件时遇到 403 签名不匹配错误,通常是因为浏览器会自动添加 Content-Type 请求头,而生成预签名 URL 时未指定该请求头,导致签名验证失败。为解决此问题,您需要在生成预签名 URL 时指定 Content-Type 请求头。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>File Upload Example</title> </head> <body> <h1>File Upload Example</h1> <!-- 选择文件 --> <input type="file" id="fileInput" /> <button id="uploadButton">Upload File</button> <script> // 请将此替换为步骤一生成的预签名 URL。 const signedUrl = "<signedUrl>"; document.getElementById('uploadButton').addEventListener('click', async () => { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert('Please select a file to upload.'); return; } try { await upload(file, signedUrl); alert('File uploaded successfully!'); } catch (error) { console.error('Error during upload:', error); alert('Upload failed: ' + error.message); } }); /** * 上传文件到 OSS * @param {File} file - 需要上传的文件 * @param {string} presignedUrl - 预签名 URL */ const upload = async (file, presignedUrl) => { const response = await fetch(presignedUrl, { method: 'PUT', body: file, // 直接上传整个文件 }); if (!response.ok) { throw new Error(`Upload failed, status: ${response.status}`); } console.log('File uploaded successfully'); }; </script> </body> </html>
C++
#include <iostream> #include <fstream> #include <curl/curl.h> void uploadFile(const std::string& signedUrl, const std::string& filePath) { CURL *curl; CURLcode res; curl_global_init(CURL_GLOBAL_DEFAULT); curl = curl_easy_init(); if (curl) { // 设置URL curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str()); // 设置请求方法为PUT curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); // 打开文件 FILE *file = fopen(filePath.c_str(), "rb"); if (!file) { std::cerr << "无法打开文件: " << filePath << std::endl; return; } // 获取文件大小 fseek(file, 0, SEEK_END); long fileSize = ftell(file); fseek(file, 0, SEEK_SET); // 设置文件大小 curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize); // 设置输入文件句柄 curl_easy_setopt(curl, CURLOPT_READDATA, file); // 执行请求 res = curl_easy_perform(curl); if (res != CURLE_OK) { std::cerr << "curl_easy_perform() 失败: " << curl_easy_strerror(res) << std::endl; } else { long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); std::cout << "返回上传状态码: " << httpCode << std::endl; if (httpCode == 200) { std::cout << "使用网络库上传成功" << std::endl; } } // 关闭文件 fclose(file); // 清理 curl_easy_cleanup(curl); } curl_global_cleanup(); } int main() { // 将<signedUrl>替换为授权URL。 std::string signedUrl = "<signedUrl>"; // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 std::string filePath = "C:\\Users\\demo.txt"; 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(); dos.close(); fis.close(); int responseCode = connection.getResponseCode(); Log.d(TAG, "返回上传状态码: " + responseCode); if (responseCode == 200) { Log.d(TAG, "使用网络库上传成功"); } return "上传完成,状态码: " + responseCode; } catch (IOException e) { e.printStackTrace(); return "上传失败: " + 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(); // 将<signedUrl>替换为授权URL。 String signedUrl = "<signedUrl>"; // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 String filePath = "C:\\Users\\demo.txt"; activity.uploadFile(signedUrl, filePath); } }
常见使用场景
使用预签名URL上传指定请求头和自定义元数据的文件
如何使用预签名URL分片上传文件
相关文档
关于预签名URL的完整示例代码,请参见GitHub示例。
关于预签名URL的API接口,请参见Presign。