客户端直传是指客户端直接上传文件到对象存储OSS。相对于服务端代理上传,客户端直传避免了业务服务器中转文件,提高了上传速度,节省了服务器资源。本文介绍客户端直传的方案优势、安全实现和实践参考。
为什么客户端直传
在典型的服务端和客户端架构下,常见的文件上传方式是服务端代理上传:客户端将文件上传到业务服务器,然后业务服务器将文件上传到OSS。在这个过程中,一份数据需要在网络上传输两次,会造成网络资源的浪费、增大服务端的资源开销。为了解决这一问题,您可以在客户端直连OSS来完成文件上传,无需经过业务服务器中转。

如何实现客户端直传
实现客户端直传需要解决以下两大问题:
跨域访问
如果您的客户端是Web端或小程序,您需要解决跨域访问被限制的问题。浏览器以及小程序容器出于安全考虑,通常都会限制跨域访问,这一限制也会限制您的客户端代码直连OSS。您可以通过配置OSS Bucket的跨域访问规则,来允许您的Web端或小程序的域名直接访问OSS。更多信息,请参见跨域设置。
安全授权
上传文件到OSS需要使用RAM用户的访问密钥(AccessKey)来完成签名认证,但是在客户端中使用长期有效的访问密钥,可能会导致访问密钥泄露,进而引起安全问题。为了解决这一问题,您可以选择以下方案实现安全上传:
服务端生成STS临时访问凭证
对于大部分上传文件的场景,建议您在服务端使用STS SDK获取STS临时访问凭证,然后在客户端使用STS临时凭证和OSS SDK直接上传文件。客户端能重复使用服务端生成的STS临时访问凭证生成签名,因此适用于基于分片上传大文件、基于分片断点续传的场景。需要注意的是,频繁地调用STS服务会引起限流,因此建议您对STS临时凭证做缓存处理,并在有效期前刷新。更多信息,请参见什么是STS。
服务端生成Post签名和Post Policy
对于要限制上传文件的属性的场景,您可以在服务端生成PostObject所需的Post签名、PostPolicy等信息,然后客户端可以凭借这些信息,在一定的限制下不依赖OSS SDK直接上传文件。您可以借助服务端生成的PostPolicy限制客户端上传的文件,例如限制文件大小、文件类型。此方案适用于通过HTML表单上传的方式上传文件。需要注意的是,此方案不支持基于分片上传大文件、基于分片断点续传的场景。更多信息,请参见PostObject。
服务端生成签名URL
对于简单上传文件的场景,您可以在服务端使用OSS SDK生成PutObject所需的签名URL,客户端可以凭借签名URL,不依赖OSS SDK直接上传文件。需要注意的是,此方案不适用于基于分片上传大文件、基于分片断点续传的场景。在服务端对每个分片生成签名URL,并将签名URL返回给客户端,会增加与服务端的交互次数和网络请求的复杂性。另外,客户端可能会修改分片的内容或顺序,导致最终合并的文件不正确。更多信息,请参见在URL中包含签名。
服务端生成STS临时访问凭证
服务端通过STS临时访问凭证授权客户端上传文件到OSS的过程如下。

客户端向业务服务器请求临时访问凭证。
业务服务器使用STS SDK调用AssumeRole接口,获取临时访问凭证。
STS生成并返回临时访问凭证给业务服务器。
业务服务器返回临时访问凭证给客户端。
客户端使用OSS SDK通过该临时访问凭证上传文件到OSS。
OSS返回成功响应给客户端。
示例工程
sts.zip
示例代码
客户端代码示例
Web端使用临时访问凭证上传文件到OSS的代码示例如下:
服务端生成Post签名和Post Policy
服务端通过Post签名和Post Policy授权客户端上传文件到OSS的过程如下。

客户端向业务服务器请求Post签名和Post Policy等信息。
业务服务器生成并返回Post签名和Post Policy等信息给客户端。
客户端使用Post签名和Post Policy等信息调用PostObject通过HTML表单的方式上传文件到OSS。
OSS返回成功响应给客户端。
示例工程
postsignature.zip
示例代码
服务端示例代码
服务端生成Post签名和Post Policy等信息的代码示例如下:
import os
from hashlib import sha1 as sha
import json
import base64
import hmac
import datetime
import time
# 配置环境变量OSS_ACCESS_KEY_ID。
access_key_id = os.environ.get('OSS_ACCESS_KEY_ID')
# 配置环境变量OSS_ACCESS_KEY_SECRET。
access_key_secret = os.environ.get('OSS_ACCESS_KEY_SECRET')
# 将<your-bucket>替换为Bucket名称。
bucket = '<your-bucket>'
# host的格式为bucketname.endpoint。将<your-bucket>替换为Bucket名称。将<your-endpoint>替换为OSS Endpoint,例如oss-cn-hangzhou.aliyuncs.com。
host = 'https://<your-bucket>.<your-endpoint>'
# 指定上传到OSS的文件前缀。
upload_dir = 'user-dir-prefix/'
# 指定过期时间,单位为秒。
expire_time = 3600
def generate_expiration(seconds):
"""
通过指定有效的时长(秒)生成过期时间。
:param seconds: 有效时长(秒)。
:return: ISO8601 时间字符串,如:"2014-12-01T12:00:00.000Z"。
"""
now = int(time.time())
expiration_time = now + seconds
gmt = datetime.datetime.utcfromtimestamp(expiration_time).isoformat()
gmt += 'Z'
return gmt
def generate_signature(access_key_secret, expiration, conditions, policy_extra_props=None):
"""
生成签名字符串Signature。
:param access_key_secret: 有权限访问目标Bucket的AccessKeySecret。
:param expiration: 签名过期时间,按照ISO8601标准表示,并需要使用UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ。示例值:"2014-12-01T12:00:00.000Z"。
:param conditions: 策略条件,用于限制上传表单时允许设置的值。
:param policy_extra_props: 额外的policy参数,后续如果policy新增参数支持,可以在通过dict传入额外的参数。
:return: signature,签名字符串。
"""
policy_dict = {
'expiration': expiration,
'conditions': conditions
}
if policy_extra_props is not None:
policy_dict.update(policy_extra_props)
policy = json.dumps(policy_dict).strip()
policy_encode = base64.b64encode(policy.encode())
h = hmac.new(access_key_secret.encode(), policy_encode, sha)
sign_result = base64.b64encode(h.digest()).strip()
return sign_result.decode()
def generate_upload_params():
policy = {
# 有效期。
"expiration": generate_expiration(expire_time),
# 约束条件。
"conditions": [
# 未指定success_action_redirect时,上传成功后的返回状态码,默认为 204。
["eq", "$success_action_status", "200"],
# 表单域的值必须以指定前缀开始。例如指定key的值以user/user1开始,则可以写为["starts-with", "$key", "user/user1"]。
["starts-with", "$key", upload_dir],
# 限制上传Object的最小和最大允许大小,单位为字节。
["content-length-range", 1, 1000000],
# 限制上传的文件为指定的图片类型
["in", "$content-type", ["image/jpg", "image/png"]]
]
}
signature = generate_signature(access_key_secret, policy.get('expiration'), policy.get('conditions'))
response = {
'policy': base64.b64encode(json.dumps(policy).encode('utf-8')).decode(),
'ossAccessKeyId': access_key_id,
'signature': signature,
'host': host,
'dir': upload_dir
# 可以在这里再自行追加其他参数
}
return json.dumps(response)
客户端示例代码
Web端使用Post签名和Post Policy等信息上传文件到OSS的代码示例如下:
const form = document.querySelector('form');
const fileInput = document.querySelector('#file');
form.addEventListener('submit', (event) => {
event.preventDefault();
let file = fileInput.files[0];
let filename = fileInput.files[0].name;
fetch('/get_post_signature_for_oss_upload', { method: 'GET' })
.then(response => response.json())
.then(data => {
const formData = new FormData();
formData.append('name',filename);
formData.append('policy', data.policy);
formData.append('OSSAccessKeyId', data.ossAccessKeyId);
formData.append('success_action_status', '200');
formData.append('signature', data.signature);
formData.append('key', data.dir + filename);
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
formData.append('file', file);
fetch(data.host, { method: 'POST', body: formData },).then((res) => {
console.log(res);
alert('文件已上传');
});
})
.catch(error => {
console.log('Error occurred while getting OSS upload parameters:', error);
});
});
服务端生成签名URL
服务端通过签名URL授权客户端上传文件到OSS的过程如下。

客户端向业务服务器请求签名URL。
业务服务器使用OSS SDK生成PUT类型的签名URL,然后将其返回给客户端。
客户端使用PUT类型的签名URL调用PutObject上传文件到OSS。
OSS向客户端返回成功响应。
示例工程
presignedurl.zip
示例代码
客户端示例代码
Web端使用签名URL上传文件到OSS的代码示例如下:
客户端直传实践参考
不同类型的客户端的直传实践参考如下: