本文介绍如何通过签名URL以及临时访问凭证OSS授权第三方直接上传文件(Object)到OSS。

前提条件

已创建存储空间(Bucket)。详情请参见创建存储空间

背景信息

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

功能优势

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

目前,您可以通过签名URL以及临时访问凭证STS授权第三方上传。

临时访问凭证

OSS可以通过阿里云STS(Security Token Service)进行临时授权访问。阿里云STS是为云计算用户提供临时访问令牌的Web服务。通过STS,您可以为第三方应用或子用户(即用户身份由您自己管理的用户)颁发一个自定义时效和权限的访问凭证。关于STS的更多信息,请参见STS介绍

STS的优势如下:

  • 您无需透露您的长期密钥(AccessKey)给第三方应用,只需生成一个访问令牌并将令牌交给第三方应用。您可以自定义这个令牌的访问权限及有效期限。
  • 您无需关心权限撤销问题,访问令牌过期后自动失效。
说明 您可以通过调用STS服务的AssumeRole接口或者使用各语言STS SDK来获取临时访问凭证。临时访问凭证包括临时访问密钥(AccessKeyId和AccessKeySecret)和安全令牌(SecurityToken)。临时访问凭证有效时间单位为秒,最小值为900,最大值以当前角色设定的最大会话时间为准。更多信息,请参见设置角色最大会话时间

使用阿里云SDK

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

// 填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 从STS服务获取的安全令牌(SecurityToken)。
String securityToken = "yourSecurityToken";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "exampledir/exampleobject.txt";

// 从STS服务获取临时访问凭证后,您可以通过临时访问密钥和安全令牌生成OSSClient。
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret, securityToken);

// 通过STS临时授权上传文件。
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File("D:\\localpath\\examplefile.txt"));
ossClient.putObject(putObjectRequest);

// 关闭OSSClient。
ossClient.shutdown();
<?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;

// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
$accessKeyId = "yourAccessKeyId";
$accessKeySecret = "yourAccessKeySecret";
// 从STS服务获取的安全令牌(SecurityToken)。
$securityToken = "yourSecurityToken";
// 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();
}            
# -*- coding: utf-8 -*-
        from aliyunsdkcore.request import CommonRequest
        from aliyunsdkcore import client
        import json
        import oss2

        # 填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
        endpoint = 'https://oss-cn-hangzhou.aliyuncs.com'
        # 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        access_key_id = 'yourAccessKeyId'
        access_key_secret = 'yourAccessKeySecret'
        # 填写Bucket名称,例如examplebucket。
        bucket_name = 'examplebucket'
        # 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        object_name = 'exampledir/exampleobject.txt'
        # 您可以登录RAM控制台,在RAM角色管理页面,搜索创建的RAM角色后,单击RAM角色名称,在RAM角色详情界面查看和复制角色的ARN信息。
        # 填写角色的ARN信息。格式为acs:ram::$accountID:role/$roleName。
        # $accountID为阿里云账号ID。您可以通过登录阿里云控制台,将鼠标悬停在右上角头像的位置,直接查看和复制账号ID,或者单击基本资料查看账号ID。
        # $roleName为RAM角色名称。您可以通过登录RAM控制台,单击左侧导航栏的RAM角色管理,在RAM角色名称列表下进行查看。
        role_arn = 'acs:ram::17464958********:role/ossststest'

        # 创建权限策略。
        # 只允许对名称为examplebucket的Bucket下的所有资源执行Putbject操作。
        policy_text = '{"Version": "1", "Statement": [{"Action": ["oss:PutObject"], "Effect": "Allow", "Resource": ["acs:oss:*:*:examplebucket/*"]}]}'

        clt = client.AcsClient(access_key_id, access_key_secret, 'cn-hangzhou')

        request = CommonRequest(product="Sts", version='2015-04-01', action_name='AssumeRole')
        request.set_method('POST')
        request.set_protocol_type('https')
        request.add_query_param('RoleArn', role_arn)
        request.add_query_param('RoleSessionName', 'SessionTest')
        # 如果Policy为空,则用户将获得该角色下所有权限。如果有特殊权限控制要求,请参考上述policy_text设置。
        request.add_query_param('Policy', policy_text)
        request.set_accept_format('JSON')

        body = clt.do_action_with_exception(request)

        # 使用RAM用户的AccessKeyId和AccessKeySecret向STS申请临时访问凭证。
        token = json.loads(oss2.to_unicode(body))

        # 使用临时访问凭证中的认证信息初始化StsAuth实例。
        auth = oss2.StsAuth(token['Credentials']['AccessKeyId'],
                            token['Credentials']['AccessKeySecret'],
                            token['Credentials']['SecurityToken'])

        # 使用StsAuth实例初始化存储空间。
        bucket = oss2.Bucket(auth, endpoint, bucket_name)

        # 上传Object。
        content = 'testabc'
        result = bucket.put_object(object_name, content)
        print('http response status: ', result.status)
                    
package main


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

func main() {    
    // 从STS服务获取的安全令牌(SecurityToken)。
    securitytoken := "yourSecurityToken"
    // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
    // 从STS服务获取临时访问凭证后,您可以通过临时访问密钥和安全令牌生成OSSClient。
    // 创建OSSClient实例。
    client, err := oss.New("yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret", oss.SecurityToken(securitytoken))
    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)
    fmt.Println(err)
}                    

签名URL

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

您可以将生成的签名URL提供给访客进行临时访问。生成签名URL时,您可以通过指定URL的过期时间来限制访客的访问时长。签名URL的默认过期时间为3600秒,最大值为32400秒。

在URL中加入签名信息,以便将该URL转给第三方实现授权访问。更多信息,请参见在URL中包含签名

使用阿里云SDK

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

// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = "yourEndpoint";
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
String accessKeyId = "yourAccessKeyId";
String accessKeySecret = "yourAccessKeySecret";
// 从STS服务获取的安全令牌(SecurityToken)。
String securityToken = "yourSecurityToken";
// 填写Bucket名称,例如examplebucket。
String bucketName = "examplebucket";
// 填写Object完整路径,例如exampleobject.txt。Object完整路径中不能包含Bucket名称。
String objectName = "exampleobject.txt";

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret, securityToken);

// 填写签名URL的过期时间。
Date expiration = null;
try {
    expiration = DateUtil.parseRfc822Date("Wed, 18 Mar 2022 14:20:00 GMT");
} catch (ParseException e) {
    e.printStackTrace();
}
// 生成签名URL。
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectName, HttpMethod.PUT);
// 设置过期时间。
request.setExpiration(expiration);
// 设置ContentType。
request.setContentType("application/txt");
// 添加用户自定义元信息。
request.addUserMetadata("author", "aliy");
// 通过HTTP PUT请求生成签名URL。
URL signedUrl = ossClient.generatePresignedUrl(request);
System.out.println("signed url for putObject: " + signedUrl);

// 使用签名URL发送请求。
// 填写本地文件的完整路径。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
File f = new File("D:\\localpath\\examplefile.txt");
FileInputStream fin = null;
try {
    fin = new FileInputStream(f);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
// 添加PutObject请求头。
Map<String, String> customHeaders = new HashMap<String, String>();
customHeaders.put("Content-Type", "application/txt");
customHeaders.put("x-oss-meta-author", "aliy");

PutObjectResult result = ossClient.putObject(signedUrl, fin, f.length(), customHeaders);

// 关闭OSSClient。
ossClient.shutdown();                           
<?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;

// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
$accessKeyId = "yourAccessKeyId";
$accessKeySecret = "yourAccessKeySecret";
// 从STS服务获取的安全令牌(SecurityToken)。
$securityToken = "yourSecurityToken";
// 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, $securityToken);

    // 生成签名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");

// 使用签名URL上传文件。
// 填写上传的字符串。
$content = "Hello OSS";
$request = new RequestCore($signedUrl);
// 生成的签名URL以PUT的方式访问。
$request->set_method('PUT');
$request->add_header('Content-Type', '');
$request->add_header('Content-Length', strlen($content));
$request->set_body($content);
$request->send_request();
$res = new ResponseCore($request->get_response_header(),
    $request->get_response_body(), $request->get_response_code());
if ($res->isOK()) {
    print(__FUNCTION__ . ": OK" . "\n");
} else {
    print(__FUNCTION__ . ": FAILED" . "\n");
};                 
# -*- coding: utf-8 -*-
import oss2

# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('yourAccessKeyId', 'yourAccessKeySecret')
# yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
# 填写Bucket名称,例如examplebucket。
bucket = oss2.Bucket(auth, 'yourEndpoint', 'examplebucket')
# 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
object_name = 'exampledir/exampleobject.txt'

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

# 使用签名URL上传本地文件。
# 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
# 如果未指定本地路径只设置了本地文件名称(例如examplefile.txt),则默认从示例程序所属项目对应本地路径中上传文件。
result = bucket.put_object_with_url_from_file(url, 'D:\\localpath\\examplefile.txt')
print(result.status)         
package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

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

func main() {
    // 获取STS临时凭证后,您可以通过其中的安全令牌(SecurityToken)和临时访问密钥(AccessKeyId和AccessKeySecret)生成OSSClient。
    client, err := oss.New("yourEndpoint", "yourAccessKeyId", "yourAccessKeySecret", oss.SecurityToken("yourSecurityToken"))
    if err != nil {
        HandleError(err)
    }
    // 填写Bucket名称,例如examplebucket。
    bucketName := "examplebucket"
    // 填写文件完整路径,例如exampledir/exampleobject.txt。文件完整路径中不能包含Bucket名称。
    objectName := "exampledir/exampleobject.txt"
    // 填写本地文件完整路径,例如D:\\localpath\\examplefile.txt,其中localpath为本地文件examplefile.txt所在本地路径。
    localFilename := "D:\\localpath\\examplefile.txt"

    // 获取存储空间。
    bucket, err := client.Bucket(bucketName)
    if err != nil {
        HandleError(err)
    }

    // 签名直传。
    signedURL, err := bucket.SignURL(objectName, oss.HTTPPut, 60)
    if err != nil {
        HandleError(err)
    }

    var val = "上云就上阿里云"
    err = bucket.PutObjectWithURL(signedURL, strings.NewReader(val))
    if err != nil {
        HandleError(err)
    }

    // 带可选参数的签名直传。请确保设置的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)
    }

    err = bucket.PutObjectFromFileWithURL(signedURL, localFilename, options...)
    if err != nil {
        HandleError(err)
    }
}
                    
using Aliyun.OSS;
using Aliyun.OSS.Common;
// 填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
var endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
var accessKeyId = "yourAccessKeyId";
var accessKeySecret = "yourAccessKeySecret";
// 填写Bucket名称,例如examplebucket。
var bucketName = "examplebucket";
// 填写Object完整路径,完整路径中不包含Bucket名称,例如exampledir/exampleobject.txt。
var objectName = "exampledir/exampleobject.txt";
var objectContent = "More than just cloud.";
// 创建OSSClient实例。
var client = new OssClient(endpoint, accessKeyId, accessKeySecret);
try
{
    // 生成签名URL。
    var generatePresignedUriRequest = new GeneratePresignedUriRequest(bucketName, objectName, SignHttpMethod.Put)
    {
        // 设置签名URL过期时间,默认值为3600秒。
        Expiration = DateTime.Now.AddHours(1),
    };
    var signedUrl = client.GeneratePresignedUri(generatePresignedUriRequest);
    // 使用签名URL上传文件。
    var buffer = Encoding.UTF8.GetBytes(objectContent);
    using (var ms = new MemoryStream(buffer))
    {
        client.PutObject(signedUrl, ms);
    }
    Console.WriteLine("Put object by signatrue succeeded. {0} ", signedUrl.ToString());
}
catch (OssException ex)
{
    Console.WriteLine("Failed with error code: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}",
        ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId);
}
catch (Exception ex)
{
    Console.WriteLine("Failed with error info: {0}", ex.Message);
}
#include <alibabacloud/oss/OssClient.h>
using namespace AlibabaCloud::OSS;

int main(void)
{
    /* 初始化OSS账号信息。*/
    /* 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。*/
    std::string AccessKeyId = "yourAccessKeyId";
    std::string AccessKeySecret = "yourAccessKeySecret";
    /* yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。*/
    std::string Endpoint = "yourEndpoint";
    /* 填写Bucket名称,例如examplebucket。*/
    std::string BucketName = "examplebucket";
    /* 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。*/   
    std::string PutobjectUrlName = "exampledir/exampleobject.txt";

     /* 初始化网络等资源。*/
    InitializeSdk();

    ClientConfiguration conf;
    OssClient client(Endpoint, AccessKeyId, AccessKeySecret, conf);

    /* 设置签名URL有效时长。*/
    std::time_t t = std::time(nullptr) + 1200;
    /* 生成签名URL。*/
    auto genOutcome = client.GeneratePresignedUrl(BucketName, PutobjectUrlName, t, Http::Put);
    if (genOutcome.isSuccess()) {
        std::cout << "GeneratePresignedUrl success, Gen url:" << genOutcome.result().c_str() << std::endl;
    }
    else {
        /* 异常处理。*/
        std::cout << "GeneratePresignedUrl fail" <<
        ",code:" << genOutcome.error().Code() <<
        ",message:" << genOutcome.error().Message() <<
        ",requestId:" << genOutcome.error().RequestId() << std::endl;
        ShutdownSdk();
        return -1;
    }

    std::shared_ptr<std::iostream> content = std::make_shared<std::stringstream>();
    *content << "test cpp sdk";

    /* 使用签名URL上传文件。*/
    auto outcome = client.PutObjectByUrl(genOutcome.result(), content);

    if (!outcome.isSuccess()) {
        /* 异常处理。*/
        std::cout << "PutObjectByUrl fail" <<
        ",code:" << outcome.error().Code() <<
        ",message:" << outcome.error().Message() <<
        ",requestId:" << outcome.error().RequestId() << std::endl;
        ShutdownSdk();
        return -1;
    }

    /* 释放网络等资源。*/
    ShutdownSdk();
    return 0;
}