文档

授权第三方上传

更新时间:

本文介绍如何通过STS或者签名URL授权第三方直接上传文件(Object)到OSS。

背景信息

在典型的Client/Server架构中,服务器端负责接收并处理客户端的请求,并将OSS作为后端的存储服务。客户端将要上传的文件发送给服务器端,然后服务器端再将数据转发上传到OSS。在这个过程中,一份数据需要在网络上传输两次,分别为从客户端到服务器端,再从服务器端到OSS。当访问量较大时,服务器端需要有足够的带宽资源来满足多个客户端同时上传的需求,这对架构的伸缩性提出了挑战。

为了解决以上场景带来的挑战,OSS提供了授权给第三方上传的功能。使用该功能,每个客户端可以直接将文件上传到OSS而不是通过服务器端转发,不仅节省了自建服务器的成本,而且充分利用了OSS的海量数据处理能力,无需考虑带宽和并发限制等,可以让客户专心于业务处理。目前,您可以通过STS或者签名URL授权第三方上传。

方式对比

方式

描述

适用场景

STS临时授权上传

当某个用户或者应用程序需要访问您的OSS资源时,您可以为其颁发一个带有限定权限的临时安全令牌(包括AccessKeyID、AccessKeySecret和SecurityToken)。通过STS方式授权,您可以在特定时间段内赋予其上传文件的权限。

授权第三方应用访问或者限制RAM用户权限的场景

签名URL授权上传

签名URL是预签名的一个HTTP(s) URL,包含了对OSS对象的操作(例如PUT)以及过期时间等信息。通过提供签名URL给第三方,第三方可以直接通过该URL上传文件至指定的OSS Bucket,而无需暴露长期有效的Access Key。

用户在Web应用直接上传文件至OSS,或者允许匿名用户上传,但需要控制上传的时间和对象名称。

STS临时授权上传

对于大部分上传文件的场景,建议您在服务端使用STS SDK获取STS临时访问凭证,然后在客户端使用STS临时凭证和OSS SDK直接上传文件。客户端能重复使用服务端生成的STS临时访问凭证生成签名,因此适用于基于分片上传大文件、基于分片断点续传的场景。需要注意的是,频繁地调用STS服务会引起限流,因此建议您对STS临时凭证做缓存处理,并在有效期前刷新。为了确保STS临时访问凭证不被客户端滥用,建议您为STS临时访问凭证添加额外的权限策略,以进一步限制其权限。更多信息,请参见什么是STS

  1. 获取临时访问凭证。

    临时访问凭证包括临时访问密钥(AccessKey ID和AccessKey Secret)和安全令牌(SecurityToken)。临时访问凭证有效时间单位为秒,最小值为900,最大值以当前角色设定的最大会话时间为准。更多信息,请参见设置RAM角色最大会话时间

    您可以通过以下两种方式获取临时访问凭证。

    • 方式一

      通过调用STS服务的AssumeRole接口获取临时访问凭证。

    • 方式二

      通过各语言STS SDK获取临时访问凭证。

  2. 使用临时访问凭证授权第三方上传文件。

    以下仅列举常见SDK通过临时访问凭证授权第三方上传文件的代码示例。关于其他SDK的通过临时访问凭证授权第三方上传文件的代码示例,请参见SDK简介

    Java

    import com.aliyun.oss.*;
    import com.aliyun.oss.model.PutObjectRequest;
    import com.aliyun.oss.common.auth.*;
    import java.io.File;
    
    public class Demo {
        public static void main(String[] args) throws Throwable {
            // 以华东1(杭州)的外网Endpoint为例,其它Region请按实际情况填写。
            String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
            // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已通过环境变量设置临时访问密钥(OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET)以及安全令牌(OSS_SESSION_TOKEN)。
            EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
            // 填写Bucket名称,例如examplebucket。
            String bucketName = "examplebucket";
            // 填写Object完整路径,例如exampleobject.txt。Object完整路径中不能包含Bucket名称。
            String objectName = "exampleobject.txt";
            // 填写本地文件完整路径。
            String pathName = "D:\\examplefile.txt";
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
    
            try {
                // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
                PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(pathName));
                ossClient.putObject(putObjectRequest);
            } catch (OSSException oe) {
                System.out.println("Caught an OSSException, which means your request made it to OSS, "
                        + "but was rejected with an error response for some reason.");
                System.out.println("Error Message:" + oe.getErrorMessage());
                System.out.println("Error Code:" + oe.getErrorCode());
                System.out.println("Request ID:" + oe.getRequestId());
                System.out.println("Host ID:" + oe.getHostId());
            } catch (ClientException ce) {
                System.out.println("Caught an ClientException, which means the client encountered "
                        + "a serious internal problem while trying to communicate with OSS, "
                        + "such as not being able to access the network.");
                System.out.println("Error Message:" + ce.getMessage());
            } finally {
                if (ossClient != null) {
                    ossClient.shutdown();
                }
            }
        }
    }

    PHP

    <?php
    if (is_file(__DIR__ . '/../autoload.php')) {
        require_once __DIR__ . '/../autoload.php';
    }
    if (is_file(__DIR__ . '/../vendor/autoload.php')) {
        require_once __DIR__ . '/../vendor/autoload.php';
    }
    
    use OSS\OssClient;
    use OSS\Core\OssException;
    
    // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已通过环境变量设置临时访问密钥(OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET)以及安全令牌(OSS_SESSION_TOKEN)。
    $accessKeyId = getenv("OSS_ACCESS_KEY_ID");
    $accessKeySecret = getenv("OSS_ACCESS_KEY_SECRET");
    $securityToken = getenv("OSS_SESSION_TOKEN");
    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    $endpoint = "yourEndpoint";
    // 填写Bucket名称。
    $bucket= "examplebucket";
    // 填写不包含Bucket名称在内的Object完整路径。
    $object = "exampleobject.txt";
    // 填写上传的字符串。
    $content = "Hello OSS";
    
    try {
        $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint, false, $securityToken);
        // 使用STS临时授权上传文件。
        $ossClient->putObject($bucket, $object, $content);
    } catch (OssException $e) {
        print $e->getMessage();
    }           

    Node.js

    const axios = require("axios");
    const OSS = require("ali-oss");
    
    // 在客户端使用临时访问凭证初始化OSS客户端,用于临时授权访问OSS资源。
    const getToken = async () => {
      // 设置客户端请求访问凭证的地址。
      await axios.get("http://localhost:8000/sts").then((token) => {
        const client = new OSS({
           // yourRegion填写Bucket所在地域。以华东1(杭州)为例,yourRegion填写为oss-cn-hangzhou。
          region: 'oss-cn-hangzhou',
          accessKeyId: token.data.AccessKeyId,
          accessKeySecret: token.data.AccessKeySecret,
          stsToken: token.data.SecurityToken,
          // 填写Bucket名称。
          bucket: "examplebucket",
          // 刷新临时访问凭证。
          refreshSTSToken: async () => {
            const refreshToken = await axios.get("http://localhost:8000/sts");
            return {
              accessKeyId: refreshToken.AccessKeyId,
              accessKeySecret: refreshToken.AccessKeySecret,
              stsToken: refreshToken.SecurityToken,
            };
          },
        });
        // 使用临时访问凭证上传文件。
        // 填写不包含Bucket名称在内的Object的完整路径,例如exampleobject.jpg。
        // 填写本地文件的完整路径,例如D:\\example.jpg。
        client.put('exampleobject.jpg', 'D:\\example.jpg').then((res)=>{console.log(res)}).catch(e=>console.log(e))
      });
    };
    getToken()

    Python

    # -*- coding: utf-8 -*-
    import oss2
    from oss2.credentials import EnvironmentVariableCredentialsProvider
    
    # yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    endpoint = 'https://oss-cn-hangzhou.aliyuncs.com'
    # 从环境变量中获取访问凭证。运行本代码示例之前,请确保已通过环境变量设置临时访问密钥(OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET)以及安全令牌(OSS_SESSION_TOKEN)。
    auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())
    # 填写Bucket名称。
    bucket_name = 'examplebucket'
    # 填写Object完整路径和字符串。Object完整路径中不能包含Bucket名称。
    object_name = 'exampleobject.txt'
    
    # 使用StsAuth实例初始化存储空间。
    bucket = oss2.Bucket(auth, endpoint, bucket_name)
    
    # 上传Object。
    result = bucket.put_object(object_name, "hello world")
    print(result.status)

    Go

    package main
    
    import (
        "fmt"
        "github.com/aliyun/aliyun-oss-go-sdk/oss"
        "os"
    )
    
    func main() {
        // 从环境变量中获取临时访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_SESSION_TOKEN。
        provider, err := oss.NewEnvironmentVariableCredentialsProvider()
        if err != nil {
            fmt.Println("Error:", err)
            os.Exit(-1)
        }
        // 创建OSSClient实例。
        // yourEndpoint填写Bucket对应的Endpoint,以华东1(杭州)为例,填写为https://oss-cn-hangzhou.aliyuncs.com。其它Region请按实际情况填写。
        client, err := oss.New("yourEndpoint", "", "", oss.SetCredentialsProvider(&provider))
        if err != nil {
            fmt.Println("Error:", err)
            os.Exit(-1)
        }
        // 填写Bucket名称,例如examplebucket。
        bucketName := "examplebucket"
        // 填写Object的完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        objectName := "exampledir/exampleobject.txt"
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        filepath := "D:\\localpath\\examplefile.txt"
        bucket,err := client.Bucket(bucketName)
        // 通过STS授权第三方上传文件。
        err = bucket.PutObjectFromFile(objectName,filepath)
        if err != nil {
            fmt.Println("Error:", err)
            os.Exit(-1)
        }
        fmt.Println("upload success")
    }

使用签名URL授权上传

对于简单上传文件的场景,您可以在服务端使用OSS SDK生成PutObject所需的签名URL,客户端可以凭借签名URL,不依赖OSS SDK直接上传文件。需要注意的是,此方案不适用于基于分片上传大文件、基于分片断点续传的场景。在服务端对每个分片生成签名URL,并将签名URL返回给客户端,会增加与服务端的交互次数和网络请求的复杂性。另外,客户端可能会修改分片的内容或顺序,导致最终合并的文件不正确。更多信息,请参见签名版本1

重要

由于STS临时账号以及签名URL均需设置有效时长,当您使用STS临时账号生成签名URL执行相关操作(例如上传、下载文件)时,以最小的有效时长为准。例如您的STS临时账号的有效时长设置为1200秒、签名URL设置为3600秒时,当有效时长超过1200秒后,您无法使用此STS临时账号生成的签名URL上传文件。

使用阿里云SDK

以下仅列举常见SDK通过签名URL授权第三方上传文件的代码示例。关于其他SDK的通过签名URL授权第三方上传文件的代码示例,请参见SDK简介

Java

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import com.aliyun.oss.model.StorageClass;
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.*;
import java.util.Date;

public class Demo {
    public static void main(String[] args) throws Throwable {
        // 以华东1(杭州)的外网Endpoint为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,例如exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "exampleobject.txt";
        // 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
        String pathName = "D:\\localpath\\examplefile.txt";

        // 创建OSSClient实例
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
        
        // 设置请求头。
        Map<String, String> headers = new HashMap<String, String>();
        /*// 指定Object的存储类型。
        headers.put(OSSHeaders.STORAGE_CLASS, StorageClass.Standard.toString());
        // 指定ContentType。
        headers.put(OSSHeaders.CONTENT_TYPE, "text/txt");*/

        // 设置用户自定义元数据。
        Map<String, String> userMetadata = new HashMap<String, String>();
        /*userMetadata.put("key1","value1");
        userMetadata.put("key2","value2");*/

        URL signedUrl = null;
        try {
            // 指定生成的签名URL过期时间,单位为毫秒。本示例以设置过期时间为1小时为例。
            Date expiration = new Date(new Date().getTime() + 3600 * 1000L);

            // 生成签名URL。
            GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectName, HttpMethod.PUT);
            // 设置过期时间。
            request.setExpiration(expiration);

            // 将请求头加入到request中。
            request.setHeaders(headers);
            // 添加用户自定义元数据。
            request.setUserMetadata(userMetadata);

            // 通过HTTP PUT请求生成签名URL。
            signedUrl = ossClient.generatePresignedUrl(request);
            // 打印签名URL。
            System.out.println("signed url for putObject: " + signedUrl);

        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        }

        // 通过签名URL临时授权简单上传文件,以HttpClients为例说明。
        putObjectWithHttp(signedUrl, pathName, headers, userMetadata);
    }

    public static void putObjectWithHttp(URL signedUrl, String pathName, Map<String, String> headers, Map<String, String> userMetadata) throws IOException {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;
        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);
            // 如果生成签名URL时设置了header参数,例如用户元数据,存储类型等,则调用签名URL上传文件时,也需要将这些参数发送至服务端。如果签名和发送至服务端的不一致,会报签名错误。
            for(Map.Entry header: headers.entrySet()){
                put.addHeader(header.getKey().toString(),header.getValue().toString());
            }
            for(Map.Entry meta: userMetadata.entrySet()){
                // 如果使用userMeta,sdk内部会为userMeta拼接"x-oss-meta-"前缀。当您使用其他方式生成签名URL进行上传时,userMeta也需要拼接"x-oss-meta-"前缀。
                put.addHeader("x-oss-meta-"+meta.getKey().toString(), meta.getValue().toString());
            }

            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();
        }
    }
}       

PHP

<?php
if (is_file(__DIR__ . '/../autoload.php')) {
    require_once __DIR__ . '/../autoload.php';
}
if (is_file(__DIR__ . '/../vendor/autoload.php')) {
    require_once __DIR__ . '/../vendor/autoload.php';
}

use OSS\OssClient;
use OSS\Core\OssException;
use OSS\Http\RequestCore;
use OSS\Http\ResponseCore;

// 运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
$accessKeyId = getenv("OSS_ACCESS_KEY_ID");
$accessKeySecret = getenv("OSS_ACCESS_KEY_SECRET");
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
$endpoint = "yourEndpoint";
// 填写Bucket名称。
$bucket= "examplebucket";
// 填写不包含Bucket名称在内的Object完整路径。
$object = "exampleobject.txt";
// 设置签名URL的有效时长为3600秒。
$timeout = 3600;
try {
    $ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint, false);

    // 生成签名URL。
    $signedUrl = $ossClient->signUrl($bucket, $object, $timeout, "PUT");
} catch (OssException $e) {
    printf(__FUNCTION__ . ": FAILED\n");
    printf($e->getMessage() . "\n");
    return;
}
print(__FUNCTION__ . ": signedUrl: " . $signedUrl . "\n");               

Node.js

const OSS = require("ali-oss");
const { default: axios } = require("axios");
const fs = require("fs");

const client = new OSS({
  // yourregion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
  region: 'yourregion',
  // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  // 填写Bucket名称。
  bucket: 'examplebucket',
});

// 生成文件的签名URL。
const url = client.signatureUrl("examplefile.txt", {
  method: "PUT",
  "Content-Type": "application/x-www-form-urlencoded",
});

// 指定本地文件的完整路径。
const file = fs.readFileSync("D:\\localpath\\examplefile.txt");

// 使用签名URL上传文件。
axios({
  url,
  method: "PUT",
  data: file,
})
  .then((r) => console.log(r))
  .catch((e) => console.log(e));

Python

# -*- coding: utf-8 -*-
import oss2
from oss2.credentials import EnvironmentVariableCredentialsProvider
import requests

# 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())

# yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
# 填写Bucket名称,例如examplebucket。
bucket = oss2.Bucket(auth, 'https://oss-cn-hangzhou.aliyuncs.com', 'examplebucket')
# 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
object_name = 'exampledir/exampleobject.txt'

# 指定Header。
headers = dict()
# 指定Content-Type。
# headers['Content-Type'] = 'text/txt'
# 指定存储类型。
# headers["x-oss-storage-class"] = "Standard"

# 生成上传文件的签名URL,有效时间为60秒。
# 生成签名URL时,OSS默认会对Object完整路径中的正斜线(/)进行转义,从而导致生成的签名URL无法直接使用。
# 设置slash_safe为True,OSS不会对Object完整路径中的正斜线(/)进行转义,此时生成的签名URL可以直接使用。
url = bucket.sign_url('PUT', object_name, 60, slash_safe=True, headers=headers)
print('签名URL的地址为:', url)

# 通过签名URL上传文件,以requests为例说明。
# 填写本地文件路径,例如D:\\exampledir\\examplefile.txt。
requests.put(url, data=open('D:\\exampledir\\examplefile.txt', 'rb').read(), headers=headers)      

Go

package main

import (
    "fmt"
    "os"
    "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func HandleError(err error) {
    fmt.Println("Error:", err)
    os.Exit(-1)
}

func main() {
	// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
	provider, err := oss.NewEnvironmentVariableCredentialsProvider()
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(-1)
	}
	// 创建OSSClient实例。
	// yourEndpoint填写Bucket对应的Endpoint,以华东1(杭州)为例,填写为https://oss-cn-hangzhou.aliyuncs.com。其它Region请按实际情况填写。
	client, err := oss.New("yourEndpoint", "", "", oss.SetCredentialsProvider(&provider))
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(-1)
	}
    // 填写Bucket名称,例如examplebucket。
    bucketName := "examplebucket"
    // 填写文件完整路径,例如exampledir/exampleobject.txt。文件完整路径中不能包含Bucket名称。
    objectName := "exampledir/exampleobject.txt"
    bucket, err := client.Bucket(bucketName)
    if err != nil {
        HandleError(err)
    }
    // 生成用于上传的签名URL,并指定签名URL的有效时间为60秒。
    signedURL, err := bucket.SignURL(objectName, oss.HTTPPut, 60)
    if err != nil {
        HandleError(err)
    }

    // 如果要在前端使用带可选参数的签名URL,请确保在服务端生成该签名URL时设置的ContentType与在前端使用时设置的ContentType一致。
    options := []oss.Option{
        oss.Meta("myprop", "mypropval"),
        oss.ContentType("text/plain"),
    }
    
    signedURL, err = bucket.SignURL(objectName, oss.HTTPPut, 60, options...)
    if err != nil {
        HandleError(err)
    }
    fmt.Printf("Sign Url:%s\n", signedURL)
}

  • 本页导读 (1)
文档反馈