在客戶端直接上傳文件到OSS
客戶端直傳是指客戶端直接上傳文件到對(duì)象存儲(chǔ)OSS。相對(duì)于服務(wù)端代理上傳,客戶端直傳避免了業(yè)務(wù)服務(wù)器中轉(zhuǎn)文件,提高了上傳速度,節(jié)省了服務(wù)器資源。本文介紹客戶端直傳的方案優(yōu)勢(shì)、安全實(shí)現(xiàn)和實(shí)踐參考。
為什么客戶端直傳
在典型的服務(wù)端和客戶端架構(gòu)下,常見的文件上傳方式是服務(wù)端代理上傳:客戶端將文件上傳到業(yè)務(wù)服務(wù)器,然后業(yè)務(wù)服務(wù)器將文件上傳到OSS。在這個(gè)過程中,一份數(shù)據(jù)需要在網(wǎng)絡(luò)上傳輸兩次,會(huì)造成網(wǎng)絡(luò)資源的浪費(fèi),增加服務(wù)端的資源開銷。為了解決這一問題,您可以在客戶端直連OSS來完成文件上傳,無需經(jīng)過業(yè)務(wù)服務(wù)器中轉(zhuǎn)。
如何實(shí)現(xiàn)客戶端直傳
實(shí)現(xiàn)客戶端直傳需要解決以下兩個(gè)大問題:
跨域訪問
如果您的客戶端是Web端或小程序,您需要解決跨域訪問被限制的問題。瀏覽器以及小程序容器出于安全考慮,通常都會(huì)限制跨域訪問,這一限制也會(huì)限制您的客戶端代碼直連OSS。您可以通過配置OSS Bucket的跨域訪問規(guī)則,來允許指定域名下的Web應(yīng)用或小程序直接訪問OSS。更多信息,請(qǐng)參見跨域設(shè)置。
安全授權(quán)
上傳文件到OSS需要使用RAM用戶的訪問密鑰(AccessKey)來完成簽名認(rèn)證,但是在客戶端中使用長期有效的訪問密鑰,可能會(huì)導(dǎo)致訪問密鑰泄露,進(jìn)而引起安全問題。為了解決這一問題,您可以選擇以下方案實(shí)現(xiàn)安全上傳:
服務(wù)端生成STS臨時(shí)訪問憑證
對(duì)于大部分上傳文件的場景,建議您在服務(wù)端使用STS SDK獲取STS臨時(shí)訪問憑證,然后在客戶端使用STS臨時(shí)憑證和OSS SDK直接上傳文件。客戶端能重復(fù)使用服務(wù)端生成的STS臨時(shí)訪問憑證生成簽名,因此適用于基于分片上傳大文件、基于分片斷點(diǎn)續(xù)傳的場景。需要注意的是,頻繁地調(diào)用STS服務(wù)會(huì)引起限流,因此建議您對(duì)STS臨時(shí)憑證做緩存處理,并在有效期前刷新。為了確保STS臨時(shí)訪問憑證不被客戶端濫用,建議您為STS臨時(shí)訪問憑證添加額外的權(quán)限策略,以進(jìn)一步限制其權(quán)限。更多信息,請(qǐng)參見什么是STS。
服務(wù)端生成PostObject所需的簽名和Post Policy
對(duì)于需要限制上傳文件屬性的場景,您可以在服務(wù)端生成PostObject所需的Post簽名、PostPolicy等信息,然后客戶端可以憑借這些信息,在一定的限制下不依賴OSS SDK直接上傳文件。您可以借助服務(wù)端生成的PostPolicy限制客戶端上傳的文件,例如限制文件大小、文件類型。此方案適用于通過HTML表單上傳的方式上傳文件。需要注意的是,此方案不支持基于分片上傳大文件、基于分片斷點(diǎn)續(xù)傳的場景。更多信息,請(qǐng)參見PostObject。
服務(wù)端生成PutObject所需的簽名URL
對(duì)于簡單上傳文件的場景,您可以在服務(wù)端使用OSS SDK生成PutObject所需的簽名URL,客戶端可以憑借簽名URL,不依賴OSS SDK直接上傳文件。需要注意的是,此方案不適用于基于分片上傳大文件、基于分片斷點(diǎn)續(xù)傳的場景。在服務(wù)端對(duì)每個(gè)分片生成簽名URL,并將簽名URL返回給客戶端,會(huì)增加與服務(wù)端的交互次數(shù)和網(wǎng)絡(luò)請(qǐng)求的復(fù)雜性。另外,客戶端可能會(huì)修改分片的內(nèi)容或順序,導(dǎo)致最終合并的文件不正確。更多信息,請(qǐng)參見簽名版本1。
服務(wù)端生成STS臨時(shí)訪問憑證
服務(wù)端通過STS臨時(shí)訪問憑證授權(quán)客戶端上傳文件到OSS的過程如下。
客戶端向業(yè)務(wù)服務(wù)器請(qǐng)求臨時(shí)訪問憑證。
業(yè)務(wù)服務(wù)器使用STS SDK調(diào)用AssumeRole接口,獲取臨時(shí)訪問憑證。
STS生成并返回臨時(shí)訪問憑證給業(yè)務(wù)服務(wù)器。
業(yè)務(wù)服務(wù)器返回臨時(shí)訪問憑證給客戶端。
客戶端使用OSS SDK通過該臨時(shí)訪問憑證上傳文件到OSS。
OSS返回成功響應(yīng)給客戶端。
示例代碼
以下示例代碼為代碼核心片段,如需查看完整代碼請(qǐng)參考示例工程:sts.zip。
服務(wù)端生成臨時(shí)訪問憑證的示例代碼如下:
當(dāng)前代碼支持一鍵部署,您可以直接在函數(shù)計(jì)算FC中一鍵部署本代碼。oss-upload-sts-app
Python
import json
from alibabacloud_tea_openapi.models import Config
from alibabacloud_sts20150401.client import Client as Sts20150401Client
from alibabacloud_sts20150401 import models as sts_20150401_models
from alibabacloud_credentials.client import Client as CredentialClient
# 將<YOUR_ROLE_ARN>替換為擁有上傳文件到指定OSS Bucket權(quán)限的RAM角色的ARN。
role_arn_for_oss_upload = '<YOUR_ROLE_ARN>'
# 將<YOUR_REGION_ID>設(shè)置為STS服務(wù)的地域,例如cn-hangzhou。
region_id = '<YOUR_REGION_ID>'
def get_sts_token():
# 初始化 CredentialClient 時(shí)不指定參數(shù),代表使用默認(rèn)憑據(jù)鏈。
# 在本地運(yùn)行程序時(shí),可以通過環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID、ALIBABA_CLOUD_ACCESS_KEY_SECRET 指定 AK;
# 在 ECS\ECI\容器服務(wù)上運(yùn)行時(shí),可以通過環(huán)境變量 ALIBABA_CLOUD_ECS_METADATA 來指定綁定的實(shí)例節(jié)點(diǎn)角色,SDK 會(huì)自動(dòng)換取 STS 臨時(shí)憑證。
config = Config(region_id=region_id, credential=CredentialClient())
sts_client = Sts20150401Client(config=config)
assume_role_request = sts_20150401_models.AssumeRoleRequest(
role_arn=role_arn_for_oss_upload,
# 將<YOUR_ROLE_SESSION_NAME>設(shè)置為自定義的會(huì)話名稱,例如oss-role-session。
role_session_name='<YOUR_ROLE_SESSION_NAME>'
)
response = sts_client.assume_role(assume_role_request)
token = json.dumps(response.body.credentials.to_map())
return token
Java
import com.aliyun.sts20150401.Client;
import com.aliyun.sts20150401.models.AssumeRoleRequest;
import com.aliyun.sts20150401.models.AssumeRoleResponse;
import com.aliyun.sts20150401.models.AssumeRoleResponseBody;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.aliyun.teaopenapi.models.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static com.aliyun.teautil.Common.assertAsString;
@RestController
public class StsController {
@Autowired
private Client stsClient;
@GetMapping("/get_sts_token_for_oss_upload")
public AssumeRoleResponseBody.AssumeRoleResponseBodyCredentials generateStsToken() {
AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest()
.setDurationSeconds(3600L)
// 將<YOUR_ROLE_SESSION_NAME>設(shè)置為自定義的會(huì)話名稱,例如 my-website-server。
.setRoleSessionName("<YOUR_ROLE_SESSION_NAME>")
// 將<YOUR_ROLE_ARN>替換為擁有上傳文件到指定OSS Bucket權(quán)限的RAM角色的ARN,可以在 RAM 角色詳情中獲得角色 ARN。
RuntimeOptions runtime = new RuntimeOptions();
try {
AssumeRoleResponse response = stsClient.assumeRoleWithOptions(assumeRoleRequest, runtime);
return response.body.credentials;
} catch (TeaException error) {
// 如有需要,請(qǐng)打印 error
assertAsString(error.message);
return null;
} catch (Exception error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 如有需要,請(qǐng)打印 error
assertAsString(error.message);
return null;
}
}
}
@Configuration
public class StsClientConfiguration {
@Bean
public Client stsClient() {
// 當(dāng)您在初始化憑據(jù)客戶端不傳入任何參數(shù)時(shí),Credentials工具會(huì)使用默認(rèn)憑據(jù)鏈方式初始化客戶端。
Config config = new Config();
config.endpoint = "sts.cn-hangzhou.aliyuncs.com";
try {
com.aliyun.credentials.Client credentials = new com.aliyun.credentials.Client();
config.setCredential(credentials);
return new Client(config);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
Go
package main
import (
"encoding/json"
"net/http"
"os"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
sts20150401 "github.com/alibabacloud-go/sts-20150401/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
/**
* 使用AK&SK初始化賬號(hào)Client
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
func CreateClient(accessKeyId *string, accessKeySecret *string) (*sts20150401.Client, error) {
config := &openapi.Config{
// 必填,您的 AccessKey ID
AccessKeyId: accessKeyId,
// 必填,您的 AccessKey Secret
AccessKeySecret: accessKeySecret,
}
// Endpoint 請(qǐng)參考 https://api.aliyun.com/product/Sts
config.Endpoint = tea.String("sts.cn-hangzhou.aliyuncs.com")
return sts20150401.NewClient(config)
}
func AssumeRole(client *sts20150401.Client) (*sts20150401.AssumeRoleResponse, error) {
assumeRoleRequest := &sts20150401.AssumeRoleRequest{
DurationSeconds: tea.Int64(3600),
RoleArn: tea.String("acs:ram::1379186349531844:role/admin-oss"),
RoleSessionName: tea.String("peiyu-demo"),
}
return client.AssumeRoleWithOptions(assumeRoleRequest, &util.RuntimeOptions{})
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, "templates/index.html")
return
} else if r.URL.Path == "/get_sts_token_for_oss_upload" {
client, err := CreateClient(tea.String(os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")), tea.String(os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")))
if err != nil {
panic(err)
}
assumeRoleResponse, err := AssumeRole(client)
if err != nil {
panic(err)
}
responseBytes, err := json.Marshal(assumeRoleResponse)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBytes)
return
}
http.NotFound(w, r)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
PHP
<?php
require_once 'vendor/autoload.php';
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use AlibabaCloud\Sts\Sts;
// 初始化Alibaba Cloud客戶端。
AlibabaCloud::accessKeyClient(getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'), getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'))
->regionId('cn-hangzhou')
->asDefaultClient();
// 創(chuàng)建STS請(qǐng)求。
$request = Sts::v20150401()->assumeRole();
// 發(fā)起STS請(qǐng)求并獲取結(jié)果。
// 將<YOUR_ROLE_SESSION_NAME>設(shè)置為自定義的會(huì)話名稱,例如oss-role-session。
// 將<YOUR_ROLE_ARN>替換為擁有上傳文件到指定OSS Bucket權(quán)限的RAM角色的ARN。
$result = $request
->withRoleSessionName("<YOUR_ROLE_SESSION_NAME>")
->withDurationSeconds(3600)
->withRoleArn("<YOUR_ROLE_ARN>")
->request();
// 獲取STS請(qǐng)求結(jié)果中的憑證信息。
$credentials = $result->get('Credentials');
// 構(gòu)建返回的JSON數(shù)據(jù)。
$response = [
'AccessKeyId' => $credentials['AccessKeyId'],
'AccessKeySecret' => $credentials['AccessKeySecret'],
'SecurityToken' => $credentials['SecurityToken'],
];
// 設(shè)置響應(yīng)頭為application/json。
header('Content-Type: application/json');
// 將結(jié)果轉(zhuǎn)換為JSON格式并打印。
echo json_encode(['Credentials' => $response]);
?>
Node.js
const express = require("express");
const { STS } = require('ali-oss');
const app = express();
const path = require("path");
app.use(express.static(path.join(__dirname, "templates")));
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_SECRET。
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_SECRET;
app.get('/get_sts_token_for_oss_upload', (req, res) => {
let sts = new STS({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret
});
// roleArn填寫步驟2獲取的角色ARN,例如acs:ram::175708322470****:role/ramtest。
// policy填寫自定義權(quán)限策略,用于進(jìn)一步限制STS臨時(shí)訪問憑證的權(quán)限。如果不指定Policy,則返回的STS臨時(shí)訪問憑證默認(rèn)擁有指定角色的所有權(quán)限。
// 3000為過期時(shí)間,單位為秒。
// sessionName用于自定義角色會(huì)話名稱,用來區(qū)分不同的令牌,例如填寫為sessiontest。
sts.assumeRole('<YOUR_ROLE_ARN>', ``, '3000', 'sessiontest').then((result) => {
console.log(result);
res.json({
AccessKeyId: result.credentials.AccessKeyId,
AccessKeySecret: result.credentials.AccessKeySecret,
SecurityToken: result.credentials.SecurityToken,
});
}).catch((err) => {
console.log(err);
res.status(400).json(err.message);
});
});
app.listen(8000, () => {
console.log("http://127.0.0.1:8000");
});
Ruby
require 'sinatra'
require 'base64'
require 'open-uri'
require 'cgi'
require 'openssl'
require 'json'
require 'sinatra/reloader'
require 'sinatra/content_for'
require 'aliyunsdkcore'
# 設(shè)置public文件夾路徑為當(dāng)前文件夾下的templates文件夾
set :public_folder, File.dirname(__FILE__) + '/templates'
def get_sts_token_for_oss_upload()
client = RPCClient.new(
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
access_key_id: ENV['ALIBABA_CLOUD_ACCESS_KEY_ID'],
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
access_key_secret: ENV['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
endpoint: 'https://sts.cn-hangzhou.aliyuncs.com',
api_version: '2015-04-01'
)
response = client.request(
action: 'AssumeRole',
params: {
# roleArn填寫步驟2獲取的角色ARN,例如acs:ram::175708322470****:role/ramtest。
"RoleArn": "acs:ram::175708322470****:role/ramtest",
# 3600為過期時(shí)間,單位為秒。
"DurationSeconds": 3600,
# sessionName用于自定義角色會(huì)話名稱,用來區(qū)分不同的令牌,例如填寫為sessiontest。
"RoleSessionName": "sessiontest"
},
opts: {
method: 'POST',
format_params: true
}
)
end
if ARGV.length == 1
$server_port = ARGV[0]
elsif ARGV.length == 2
$server_ip = ARGV[0]
$server_port = ARGV[1]
end
$server_ip = "0.0.0.0"
$server_port = 8000
puts "App server is running on: http://#{$server_ip}:#{$server_port}"
set :bind, $server_ip
set :port, $server_port
get '/get_sts_token_for_oss_upload' do
token = get_sts_token_for_oss_upload()
response = {
"AccessKeyId" => token["Credentials"]["AccessKeyId"],
"AccessKeySecret" => token["Credentials"]["AccessKeySecret"],
"SecurityToken" => token["Credentials"]["SecurityToken"]
}
response.to_json
end
get '/*' do
puts "********************* GET "
send_file File.join(settings.public_folder, 'index.html')
end
C#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Aliyun.OSS;
using System;
using System.IO;
using AlibabaCloud.SDK.Sts20150401;
using System.Text.Json;
namespace YourNamespace
{
public class Program
{
private ILogger<Program> _logger;
public static AlibabaCloud.SDK.Sts20150401.Client CreateClient(string accessKeyId, string accessKeySecret)
{
var config = new AlibabaCloud.OpenApiClient.Models.Config
{
AccessKeyId = accessKeyId,
AccessKeySecret = accessKeySecret,
Endpoint = "sts.cn-hangzhou.aliyuncs.com"
};
return new AlibabaCloud.SDK.Sts20150401.Client(config);
}
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
builder.Logging.AddConsole();
var serviceProvider = builder.Services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
app.UseStaticFiles();
app.MapGet("/", async (context) =>
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "templates/index.html");
var htmlContent = await File.ReadAllTextAsync(filePath);
await context.Response.WriteAsync(htmlContent);
logger.LogInformation("GET request to root path");
});
app.MapGet("/get_sts_token_for_oss_upload", async (context) =>
{
var program = new Program(logger);
var client = CreateClient(Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID"), Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
var assumeRoleRequest = new AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleRequest();
// 將<YOUR_ROLE_SESSION_NAME>設(shè)置為自定義的會(huì)話名稱,例如oss-role-session。
assumeRoleRequest.RoleSessionName = "<YOUR_ROLE_SESSION_NAME>";
// 將<YOUR_ROLE_ARN>替換為擁有上傳文件到指定OSS Bucket權(quán)限的RAM角色的ARN。
assumeRoleRequest.RoleArn = "<YOUR_ROLE_ARN>";
assumeRoleRequest.DurationSeconds = 3600;
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
var response = client.AssumeRoleWithOptions(assumeRoleRequest, runtime);
var credentials = response.Body.Credentials;
var jsonResponse = JsonSerializer.Serialize(new
{
AccessKeyId = credentials.AccessKeyId,
AccessKeySecret = credentials.AccessKeySecret,
Expiration = credentials.Expiration,
SecurityToken = credentials.SecurityToken
});
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(jsonResponse);
});
app.Run();
}
public Program(ILogger<Program> logger)
{
_logger = logger;
}
}
}
Web端使用臨時(shí)訪問憑證上傳文件到OSS的示例代碼如下:
let credentials = null;
const form = document.querySelector("form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
// 臨時(shí)憑證過期時(shí),才重新獲取,減少對(duì)STS服務(wù)的調(diào)用。
if (isCredentialsExpired(credentials)) {
const response = await fetch("/get_sts_token_for_oss_upload", {
method: "GET",
});
if (!response.ok) {
// 處理錯(cuò)誤的HTTP狀態(tài)碼。
throw new Error(
`獲取STS令牌失敗: ${response.status} ${response.statusText}`
);
}
credentials = await response.json();
}
const client = new OSS({
// 將<YOUR_BUCKET>設(shè)置為OSS Bucket名稱。
bucket: "<YOUR_BUCKET>",
// 將<YOUR_REGION>設(shè)置為OSS Bucket所在地域,例如region: 'oss-cn-hangzhou'。
region: "oss-<YOUR_REGION>",
accessKeyId: credentials.AccessKeyId,
accessKeySecret: credentials.AccessKeySecret,
stsToken: credentials.SecurityToken,
});
const fileInput = document.querySelector("#file");
const file = fileInput.files[0];
const result = await client.put(file.name, file);
console.log(result);
});
/**
* 判斷臨時(shí)憑證是否到期。
**/
function isCredentialsExpired(credentials) {
if (!credentials) {
return true;
}
const expireDate = new Date(credentials.Expiration);
const now = new Date();
// 如果有效期不足一分鐘,視為過期。
return expireDate.getTime() - now.getTime() <= 60000;
}
服務(wù)端生成PostObject所需的簽名和Post Policy
服務(wù)端通過Post簽名和Post Policy授權(quán)客戶端上傳文件到OSS的過程如下。
客戶端向業(yè)務(wù)服務(wù)器請(qǐng)求Post簽名和Post Policy等信息。
業(yè)務(wù)服務(wù)器生成并返回Post簽名和Post Policy等信息給客戶端。
客戶端使用Post簽名和Post Policy等信息調(diào)用PostObject通過HTML表單的方式上傳文件到OSS。
OSS返回成功響應(yīng)給客戶端。
示例代碼
以下示例代碼為代碼核心片段,如需查看完整代碼請(qǐng)參考示例工程:postsignature.zip。
服務(wù)端生成Post簽名和Post Policy等信息的示例代碼如下:
當(dāng)前代碼支持一鍵部署,您可以直接在函數(shù)計(jì)算FC中一鍵部署本代碼。oss-upload-post-signature-app
Python
import os
from hashlib import sha1 as sha
import json
import base64
import hmac
import datetime
import time
# 配置環(huán)境變量OSS_ACCESS_KEY_ID。
access_key_id = os.environ.get('OSS_ACCESS_KEY_ID')
# 配置環(huán)境變量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/'
# 指定過期時(shí)間,單位為秒。
expire_time = 3600
def generate_expiration(seconds):
"""
通過指定有效的時(shí)長(秒)生成過期時(shí)間。
:param seconds: 有效時(shí)長(秒)。
:return: ISO8601 時(shí)間字符串,如:"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: 有權(quán)限訪問目標(biāo)Bucket的AccessKeySecret。
:param expiration: 簽名過期時(shí)間,按照ISO8601標(biāo)準(zhǔn)表示,并需要使用UTC時(shí)間,格式為yyyy-MM-ddTHH:mm:ssZ。示例值:"2014-12-01T12:00:00.000Z"。
:param conditions: 策略條件,用于限制上傳表單時(shí)允許設(shè)置的值。
:param policy_extra_props: 額外的policy參數(shù),后續(xù)如果policy新增參數(shù)支持,可以在通過dict傳入額外的參數(shù)。
: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時(shí),上傳成功后的返回狀態(tài)碼,默認(rèn)為 204。
["eq", "$success_action_status", "200"],
# 表單域的值必須以指定前綴開始。例如指定key的值以u(píng)ser/user1開始,則可以寫為["starts-with", "$key", "user/user1"]。
["starts-with", "$key", upload_dir],
# 限制上傳Object的最小和最大允許大小,單位為字節(jié)。
["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
# 可以在這里再自行追加其他參數(shù)
}
return json.dumps(response)
Java
import com.aliyun.help.demo.uploading_to_oss_directly_postsignature.config.OssConfig;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.codehaus.jettison.json.JSONObject;
import java.util.Date;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import javax.annotation.PreDestroy;
@Controller
public class PostSignatureController {
@Autowired
private OSS ossClient;
@Autowired
private OssConfig ossConfig;
@GetMapping("/get_post_signature_for_oss_upload")
@ResponseBody
public String generatePostSignature() {
JSONObject response = new JSONObject();
try {
long expireEndTime = System.currentTimeMillis() + ossConfig.getExpireTime() * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, ossConfig.getDir());
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
response.put("ossAccessKeyId", ossConfig.getAccessKeyId());
response.put("policy", encodedPolicy);
response.put("signature", postSignature);
response.put("dir", ossConfig.getDir());
response.put("host", ossConfig.getHost());
} 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.");
// 假設(shè)此方法存在
System.out.println("HTTP Status Code: " + oe.getRawResponseError());
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();
}
return response.toString();
}
}
}
@Configuration
public class OssConfig {
/**
* 將 <YOUR-ENDPOINT> 替換為 Endpoint,例如 oss-cn-hangzhou.aliyuncs.com
*/
private String endpoint = "<YOUR-ENDPOINT>";
/**
* 將 <YOUR-BUCKET> 替換為 Bucket 名稱
*/
private String bucket = "<YOUR-BUCKET>";
/**
* 指定上傳到 OSS 的文件前綴
*/
private String dir = "user-dir-prefix/";
/**
* 指定過期時(shí)間,單位為秒
*/
private long expireTime = 3600;
/**
* 構(gòu)造 host
*/
private String host = "http://" + bucket + "." + endpoint;
/**
* 通過環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID 設(shè)置 accessKeyId
*/
private String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
/**
* 通過環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_SECRET 設(shè)置 accessKeySecret
*/
private String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
private OSS ossClient;
@Bean
public OSS getOssClient() {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
return ossClient;
}
@Bean
public String getHost() {
return host;
}
@Bean
public String getAccessKeyId() {
return accessKeyId;
}
@Bean
public long getExpireTime() {
return expireTime;
}
@Bean
public String getDir() {
return dir;
}
@PreDestroy
public void onDestroy() {
ossClient.shutdown();
}
}
Go
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
var (
// 配置環(huán)境變量OSS_ACCESS_KEY_ID。
accessKeyId = os.Getenv("OSS_ACCESS_KEY_ID")
// 配置環(huán)境變量OSS_ACCESS_KEY_SECRET。
accessKeySecret = os.Getenv("OSS_ACCESS_KEY_SECRET")
// host的格式為bucketname.endpoint。將${your-bucket}替換為Bucket名稱。將${your-endpoint}替換為OSS Endpoint,例如oss-cn-hangzhou.aliyuncs.com。
host = "http://${your-bucket}.${your-endpoint}"
// 指定上傳到OSS的文件前綴。
uploadDir = "user-dir-prefix/"
// 指定過期時(shí)間,單位為秒。
expireTime = int64(3600)
)
type ConfigStruct struct {
Expiration string `json:"expiration"`
Conditions [][]interface{} `json:"conditions"`
}
type PolicyToken struct {
AccessKeyId string `json:"ossAccessKeyId"`
Host string `json:"host"`
Signature string `json:"signature"`
Policy string `json:"policy"`
Directory string `json:"dir"`
}
func getGMTISO8601(expireEnd int64) string {
return time.Unix(expireEnd, 0).UTC().Format("2006-01-02T15:04:05Z")
}
func getPolicyToken() string {
now := time.Now().Unix()
expireEnd := now + expireTime
tokenExpire := getGMTISO8601(expireEnd)
var config ConfigStruct
config.Expiration = tokenExpire
// 添加文件前綴限制
config.Conditions = append(config.Conditions, []interface{}{"starts-with", "$key", uploadDir})
// 添加文件大小限制,例如1KB到10MB
minSize := int64(1024)
maxSize := int64(10 * 1024 * 1024)
config.Conditions = append(config.Conditions, []interface{}{"content-length-range", minSize, maxSize})
result, err := json.Marshal(config)
if err != nil {
fmt.Println("callback json err:", err)
return ""
}
encodedResult := base64.StdEncoding.EncodeToString(result)
h := hmac.New(sha1.New, []byte(accessKeySecret))
io.WriteString(h, encodedResult)
signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil))
policyToken := PolicyToken{
AccessKeyId: accessKeyId,
Host: host,
Signature: signedStr,
Policy: encodedResult,
Directory: uploadDir,
}
response, err := json.Marshal(policyToken)
if err != nil {
fmt.Println("json err:", err)
return ""
}
return string(response)
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, "templates/index.html")
return
} else if r.URL.Path == "/get_post_signature_for_oss_upload" {
policyToken := getPolicyToken()
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(policyToken))
return
}
http.NotFound(w, r)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
PHP
<?php
function gmt_iso8601($time)
{
return str_replace('+00:00', '.000Z', gmdate('c', $time));
}
// 從環(huán)境變量中獲取訪問憑證。運(yùn)行本示例代碼之前,請(qǐng)確保已設(shè)置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
$accessKeyId = getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
$accessKeySecret = getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
// $host的格式為<YOUR-BUCKET>.<YOUR-ENDPOINT>',請(qǐng)?zhí)鎿Q為您的真實(shí)信息。
$host = 'http://<YOUR-BUCKET>.<YOUR-ENDPOINT>';
// 用戶上傳文件時(shí)指定的前綴。
$dir = 'user-dir-prefix/';
$now = time();
//設(shè)置該policy超時(shí)時(shí)間是10s. 即這個(gè)policy過了這個(gè)有效時(shí)間,將不能訪問。
$expire = 30;
$end = $now + $expire;
$expiration = gmt_iso8601($end);
//最大文件大小.用戶可以自己設(shè)置。
$condition = array(0 => 'content-length-range', 1 => 0, 2 => 1048576000);
$conditions[] = $condition;
// 表示用戶上傳的數(shù)據(jù),必須是以$dir開始,不然上傳會(huì)失敗,這一步不是必須項(xiàng),只是為了安全起見,防止用戶通過policy上傳到別人的目錄。
$start = array(0 => 'starts-with', 1 => '$key', 2 => $dir);
$conditions[] = $start;
$arr = array('expiration' => $expiration, 'conditions' => $conditions);
$policy = json_encode($arr);
$base64_policy = base64_encode($policy);
$string_to_sign = $base64_policy;
$signature = base64_encode(hash_hmac('sha1', $string_to_sign, $accessKeySecret, true));
$response = array();
$response['ossAccessKeyId'] = $accessKeyId;
$response['host'] = $host;
$response['policy'] = $base64_policy;
$response['signature'] = $signature;
$response['dir'] = $dir;
echo json_encode($response);
Node.js
const express = require("express");
const { Buffer } = require("buffer");
const OSS = require("ali-oss");
const app = express();
const path = require("path");
const config = {
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
// 將<YOUR-BUCKET>替換為Bucket名稱。
bucket: "<YOUR-BUCKET>",
// 指定上傳到OSS的文件前綴。
dir: "prefix/",
};
app.use(express.static(path.join(__dirname, "templates")));
app.get("/get_post_signature_for_oss_upload", async (req, res) => {
const client = new OSS(config);
const date = new Date();
// 設(shè)置簽名的有效期,單位為秒。
date.setSeconds(date.getSeconds() + 3600);
const policy = {
expiration: date.toISOString(),
conditions: [
// 設(shè)置上傳文件的大小限制。
["content-length-range", 0, 1048576000],
// 限制可上傳的Bucket。
{ bucket: client.options.bucket },
],
};
const formData = await client.calculatePostSignature(policy);
const host = `http://${config.bucket}.${
(await client.getBucketLocation()).location
}.aliyuncs.com`.toString();
const params = {
policy: formData.policy,
signature: formData.Signature,
ossAccessKeyId: formData.OSSAccessKeyId,
host,
dir: config.dir,
};
res.json(params);
});
app.get(/^(.+)*\.(html|js)$/i, async (req, res) => {
res.sendFile(path.join(__dirname, "./templates", req.originalUrl));
});
app.listen(8000, () => {
console.log("http://127.0.0.1:8000");
});
Ruby
require 'sinatra'
require 'base64'
require 'open-uri'
require 'cgi'
require 'openssl'
require 'json'
require 'sinatra/reloader'
require 'sinatra/content_for'
# 設(shè)置public文件夾路徑為當(dāng)前文件夾下的templates文件夾
set :public_folder, File.dirname(__FILE__) + '/templates'
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
$access_key_id = ENV['ALIBABA_CLOUD_ACCESS_ID']
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
$access_key_secret = ENV['ALIBABA_CLOUD_ACCESS_SECRET']
# $host的格式為<bucketname>.<endpoint>,請(qǐng)?zhí)鎿Q為您的真實(shí)信息。
$host = 'http://<bucketname>.<endpoint>';
# 用戶上傳文件時(shí)指定的前綴。
$upload_dir = 'user-dir-prefix/'
# 過期時(shí)間,單位為秒。
$expire_time = 30
$server_ip = "0.0.0.0"
$server_port = 8000
if ARGV.length == 1
$server_port = ARGV[0]
elsif ARGV.length == 2
$server_ip = ARGV[0]
$server_port = ARGV[1]
end
puts "App server is running on: http://#{$server_ip}:#{$server_port}"
def hash_to_jason(source_hash)
jason_string = source_hash.to_json;
jason_string.gsub!("\":[", "\": [")
jason_string.gsub!("\",\"", "\", \"")
jason_string.gsub!("],\"", "], \"")
jason_string.gsub!("\":\"", "\": \"")
jason_string
end
def get_token()
expire_syncpoint = Time.now.to_i + $expire_time
expire = Time.at(expire_syncpoint).utc.iso8601()
response.headers['expire'] = expire
policy_dict = {}
condition_arrary = Array.new
array_item = Array.new
array_item.push('starts-with')
array_item.push('$key')
array_item.push($upload_dir)
condition_arrary.push(array_item)
policy_dict["conditions"] = condition_arrary
policy_dict["expiration"] = expire
policy = hash_to_jason(policy_dict)
policy_encode = Base64.strict_encode64(policy).chomp;
h = OpenSSL::HMAC.digest('sha1', $access_key_secret, policy_encode)
hs = Digest::MD5.hexdigest(h)
sign_result = Base64.strict_encode64(h).strip()
token_dict = {}
token_dict['ossAccessKeyId'] = $access_key_id
token_dict['host'] = $host
token_dict['policy'] = policy_encode
token_dict['signature'] = sign_result
token_dict['expire'] = expire_syncpoint
token_dict['dir'] = $upload_dir
result = hash_to_jason(token_dict)
result
end
set :bind, $server_ip
set :port, $server_port
get '/get_post_signature_for_oss_upload' do
token = get_token()
puts "Token: #{token}"
token
end
get '/*' do
puts "********************* GET "
send_file File.join(settings.public_folder, 'index.html')
end
end
if ARGV.length == 1
$server_port = ARGV[0]
elsif ARGV.length == 2
$server_ip = ARGV[0]
$server_port = ARGV[1]
end
$server_ip = "0.0.0.0"
$server_port = 8000
puts "App server is running on: http://#{$server_ip}:#{$server_port}"
set :bind, $server_ip
set :port, $server_port
get '/get_sts_token_for_oss_upload' do
token = get_sts_token_for_oss_upload()
response = {
"AccessKeyId" => token["Credentials"]["AccessKeyId"],
"AccessKeySecret" => token["Credentials"]["AccessKeySecret"],
"SecurityToken" => token["Credentials"]["SecurityToken"]
}
response.to_json
end
get '/*' do
puts "********************* GET "
send_file File.join(settings.public_folder, 'index.html')
end
C#
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Collections.Generic;
using System;
using System.Globalization;
using System.Text;
using System.Security.Cryptography;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace YourNamespace
{
public class Program
{
private ILogger<Program> _logger;
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
public string AccessKeyId { get; set; } = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID");
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
public string AccessKeySecret { get; set; } = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
// host的格式為bucketname.endpoint。將<YOUR-BUCKET>替換為Bucket名稱。將<YOUR-ENDPOINT>替換為OSS Endpoint,例如oss-cn-hangzhou.aliyuncs.com。
public string Host { get; set; } = "<YOUR-BUCKET>.<YOUR-ENDPOINT>";
// 指定上傳到OSS的文件前綴。
public string UploadDir { get; set; } = "user-dir-prefix/";
// 指定過期時(shí)間,單位為秒。
public int ExpireTime { get; set; } = 3600;
public class PolicyConfig
{
public string expiration { get; set; }
public List<List<object>> conditions { get; set; }
}
public class PolicyToken
{
public string Accessid { get; set; }
public string Policy { get; set; }
public string Signature { get; set; }
public string Dir { get; set; }
public string Host { get; set; }
public string Expire { get; set; }
}
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
builder.Logging.AddConsole();
var logger = builder.Services.BuildServiceProvider().GetRequiredService<ILogger<Program>>();
app.UseStaticFiles();
app.MapGet("/", async (context) =>
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "templates/index.html");
var htmlContent = await File.ReadAllTextAsync(filePath);
await context.Response.WriteAsync(htmlContent);
logger.LogInformation("GET request to root path");
});
app.MapGet("/get_post_signature_for_oss_upload", async (context) =>
{
var program = new Program(logger);
var token = program.GetPolicyToken();
logger.LogInformation($"Token: {token}");
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(token);
});
app.Run();
}
public Program(ILogger<Program> logger)
{
_logger = logger;
}
private string ToUnixTime(DateTime dateTime)
{
return ((DateTimeOffset)dateTime).ToUnixTimeSeconds().ToString();
}
private string GetPolicyToken()
{
var expireDateTime = DateTime.Now.AddSeconds(ExpireTime);
var config = new PolicyConfig
{
expiration = FormatIso8601Date(expireDateTime),
conditions = new List<List<object>>()
};
config.conditions.Add(new List<object>
{
"content-length-range", 0, 1048576000
});
var policy = JsonConvert.SerializeObject(config);
var policyBase64 = EncodeBase64("utf-8", policy);
var signature = ComputeSignature(AccessKeySecret, policyBase64);
var policyToken = new PolicyToken
{
Accessid = AccessKeyId,
Host = Host,
Policy = policyBase64,
Signature = signature,
Expire = ToUnixTime(expireDateTime),
Dir = UploadDir
};
return JsonConvert.SerializeObject(policyToken);
}
private string FormatIso8601Date(DateTime dtime)
{
return dtime.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'",
CultureInfo.CurrentCulture);
}
private string EncodeBase64(string codeType, string code)
{
string encode = "";
byte[] bytes = Encoding.GetEncoding(codeType).GetBytes(code);
try
{
encode = Convert.ToBase64String(bytes);
}
catch
{
encode = code;
}
return encode;
}
private string ComputeSignature(string key, string data)
{
using (var algorithm = new HMACSHA1(Encoding.UTF8.GetBytes(key)))
{
return Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(data)));
}
}
}
}
Web端使用Post簽名和Post Policy等信息上傳文件到OSS的示例代碼如下:
const form = document.querySelector("form");
const fileInput = document.querySelector("#file");
form.addEventListener("submit", (event) => {
event.preventDefault();
const file = fileInput.files[0];
const filename = fileInput.files[0].name;
fetch("/get_post_signature_for_oss_upload", { method: "GET" })
.then((response) => {
if (!response.ok) {
throw new Error("獲取簽名失敗");
}
return 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);
formData.append("file", file);
return fetch(data.host, { method: "POST", body: formData });
})
.then((response) => {
if (response.ok) {
console.log("上傳成功");
alert("文件已上傳");
} else {
console.log("上傳失敗", response);
alert("上傳失敗,請(qǐng)稍后再試");
}
})
.catch((error) => {
console.error("發(fā)生錯(cuò)誤:", error);
});
});
服務(wù)端生成PutObject所需的簽名URL
服務(wù)端通過簽名URL授權(quán)客戶端上傳文件到OSS的過程如下。
客戶端向業(yè)務(wù)服務(wù)器請(qǐng)求簽名URL。
業(yè)務(wù)服務(wù)器使用OSS SDK生成PUT類型的簽名URL,然后將其返回給客戶端。
客戶端使用PUT類型的簽名URL調(diào)用PutObject上傳文件到OSS。
OSS向客戶端返回成功響應(yīng)。
示例代碼
以下示例代碼為代碼核心片段,如需查看完整代碼請(qǐng)參考示例工程:presignedurl.zip。
服務(wù)端生成簽名URL的示例代碼如下:
當(dāng)前代碼支持一鍵部署,您可以直接在函數(shù)計(jì)算FC中一鍵部署本代碼。oss-upload-presigned-url-app
Python
import oss2
from oss2.credentials import EnvironmentVariableCredentialsProvider
# 從環(huán)境變量中獲取訪問憑證。運(yùn)行本示例代碼之前,請(qǐng)確保已設(shè)置環(huán)境變量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())
# 將<YOUR_ENDPOINT>替換為Bucket所在地域?qū)?yīng)的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
# 將<YOUR_BUCKET>替換為Bucket名稱。
bucket = oss2.Bucket(auth, '<YOUR_ENDPOINT>', '<YOUR_BUCKET>')
# 指定過期時(shí)間,單位秒。
expire_time = 3600
# 填寫Object完整路徑,例如exampledir/exampleobject.png。Object完整路徑中不能包含Bucket名稱。
object_name = 'exampledir/exampleobject.png'
def generate_presigned_url():
# 指定Header。
headers = dict()
# 指定Content-Type。
headers['Content-Type'] = 'image/png'
# 指定存儲(chǔ)類型。
# headers["x-oss-storage-class"] = "Standard"
# 生成簽名URL時(shí),OSS默認(rèn)會(huì)對(duì)Object完整路徑中的正斜線(/)進(jìn)行轉(zhuǎn)義,從而導(dǎo)致生成的簽名URL無法直接使用。
# 設(shè)置slash_safe為True,OSS不會(huì)對(duì)Object完整路徑中的正斜線(/)進(jìn)行轉(zhuǎn)義,此時(shí)生成的簽名URL可以直接使用。
url = bucket.sign_url('PUT', object_name, expire_time, slash_safe=True, headers=headers)
return url
Java
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import com.aliyun.oss.HttpMethod;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.net.URL;
import java.util.Date;
import javax.annotation.PreDestroy;
@Configuration
public class OssConfig {
/**
* 將<your-endpoint>替換為OSS Endpoint,例如oss-cn-hangzhou.aliyuncs.com。
*/
private static final String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
/**
* 通過環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID 設(shè)置 accessKeyId
*/
@Value("${ALIBABA_CLOUD_ACCESS_KEY_ID}")
private String accessKeyId;
/**
* 通過環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_Secret 設(shè)置 accessKeySecret
*/
@Value("${ALIBABA_CLOUD_ACCESS_KEY_SECRET}")
private String accessKeySecret;
private OSS ossClient;
@Bean
public OSS getSssClient() {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
return ossClient;
}
@PreDestroy
public void onDestroy() {
ossClient.shutdown();
}
}
@Controller
public class PresignedURLController {
/**
* 將<your-bucket>替換為Bucket名稱。
* 指定上傳到OSS的文件前綴。
* 將<your-object>替換為Object完整路徑,例如exampleobject.txt。Object完整路徑中不能包含Bucket名稱。
* 指定過期時(shí)間,單位為毫秒。
*/
private static final String BUCKET_NAME = "<your-bucket>";
private static final String OBJECT_NAME = "<your-object>";
private static final long EXPIRE_TIME = 3600 * 1000L;
@Autowired
private OSS ossClient;
@GetMapping("/get_presigned_url_for_oss_upload")
@ResponseBody
public String generatePresignedURL() {
try {
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(BUCKET_NAME, OBJECT_NAME, HttpMethod.PUT);
Date expiration = new Date(System.currentTimeMillis() + EXPIRE_TIME);
request.setExpiration(expiration);
request.setContentType("image/png");
URL signedUrl = ossClient.generatePresignedUrl(request);
return signedUrl.toString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Go
package main
import (
"fmt"
"net/http"
"os"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
func getURL() string {
// yourEndpoint填寫B(tài)ucket對(duì)應(yīng)的Endpoint,以華東1(杭州)為例,填寫為https://oss-cn-hangzhou.aliyuncs.com。其它Region請(qǐng)按實(shí)際情況填寫。
endpoint := "https://oss-cn-beijing.aliyuncs.com"
// 填寫B(tài)ucket名稱,例如examplebucket。
bucketName := "examplebucket"
// 填寫文件完整路徑,例如exampledir/exampleobject.txt。文件完整路徑中不能包含Bucket名稱。
objectName := "exampledir/exampleobject.txt"
// 從環(huán)境變量中獲取訪問憑證。運(yùn)行本示例代碼之前,請(qǐng)確保已設(shè)置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
accessKeyID := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
accessKeySecret := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
client, err := oss.New(endpoint, accessKeyID, accessKeySecret)
if err != nil {
fmt.Println("json err:", err)
}
bucket, err := client.Bucket(bucketName)
if err != nil {
fmt.Println("json err:", err)
}
options := []oss.Option{
oss.ContentType("image/png"),
}
signedURL, err := bucket.SignURL(objectName, oss.HTTPPut, 60, options...)
if err != nil {
fmt.Println("json err:", err)
}
return signedURL
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, "templates/index.html")
return
} else if r.URL.Path == "/get_presigned_url_for_oss_upload" {
url := getURL()
fmt.Fprintf(w, "%s", url)
return
}
http.NotFound(w, r)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
PHP
<?php
require_once __DIR__ . '/vendor/autoload.php';
use OSS\OssClient;
use OSS\Core\OssException;
use OSS\Http\RequestCore;
use OSS\Http\ResponseCore;
// 運(yùn)行本示例代碼之前,請(qǐng)確保已設(shè)置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET。
$accessKeyId = getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
$accessKeySecret = getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
// yourEndpoint填寫B(tài)ucket所在地域?qū)?yīng)的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
$endpoint = "<YOUR-ENDPOINT>";
// 填寫B(tài)ucket名稱。
$bucket= "<YOUR-BUCKET>";
// 填寫不包含Bucket名稱在內(nèi)的Object完整路徑。
$object = "test.png";
// 設(shè)置簽名URL的有效時(shí)長為3600秒。
$timeout = 3600;
try {
$ossClient = new OssClient($accessKeyId, $accessKeySecret, $endpoint, false);
// 生成簽名URL。
$signedUrl = $ossClient->signUrl($bucket, $object, $timeout, "PUT", array('Content-Type' => 'image/png'));
// 打印返回的數(shù)據(jù)
echo $signedUrl;
} catch (OssException $e) {
printf($e->getMessage() . "\n");
return;
}
Node.js
const express = require("express");
const { Buffer } = require("buffer");
const OSS = require("ali-oss");
const app = express();
const path = require("path");
const fs = require("fs");
const axios = require("axios");
const config = {
// <YOURREGION>填寫B(tài)ucket所在地域。以華東1(杭州)為例,Region填寫為oss-cn-hangzhou。
region: '<YOURREGION>',
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
// 將<YOUR-BUCKET>替換為Bucket名稱。
bucket: "<YOUR-BUCKET>",
}
const object = "examplefile.png";
app.use(express.static(path.join(__dirname, "templates")));
app.get("/get_presigned_url_for_oss_upload", async (req, res) => {
const client = new OSS(config);
const url = client.signatureUrl(object, {
method: "PUT",
"Content-Type": "application/x-www-form-urlencoded",
});
res.send(url);
console.log(url);
});
app.listen(8000, () => {
console.log("http://127.0.0.1:8000");
});
Ruby
require 'sinatra'
require 'base64'
require 'open-uri'
require 'cgi'
require 'openssl'
require 'json'
require 'sinatra/reloader'
require 'sinatra/content_for'
require 'aliyun/oss'
include Aliyun::OSS
# 設(shè)置public文件夾路徑為當(dāng)前文件夾下的templates文件夾
set :public_folder, File.dirname(__FILE__) + '/templates'
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
$access_key_id = ENV['ALIBABA_CLOUD_ACCESS_KEY_ID']
# 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_SECRET。
$access_key_secret = ENV['ALIBABA_CLOUD_ACCESS_KEY_SECRET']
# 填寫Object完整路徑,例如exampledir/exampleobject.png。Object完整路徑中不能包含Bucket名稱。
object_key = 'exampledir/exampleobject.png'
def get_presigned_url(client, object_key)
# 將<YOUR-BUCKET>替換為Bucket名稱。
bucket = client.get_bucket('<YOUR-BUCKET>')
# 生成簽名URL,并指定URL有效時(shí)間為1小時(shí)(3600秒)。
bucket.object_url(object_key, 3600)
end
client = Aliyun::OSS::Client.new(
# 將<YOUR-ENDPOINT>替換為Bucket所在地域?qū)?yīng)的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
endpoint: '<YOUR-ENDPOINT>',
# 從環(huán)境變量中獲取訪問憑證。運(yùn)行本示例代碼之前,請(qǐng)確保已設(shè)置環(huán)境變量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
access_key_id: $access_key_id,
access_key_secret: $access_key_secret
)
if ARGV.length == 1
$server_port = ARGV[0]
elsif ARGV.length == 2
$server_ip = ARGV[0]
$server_port = ARGV[1]
end
$server_ip = "0.0.0.0"
$server_port = 8000
puts "App server is running on: http://#{$server_ip}:#{$server_port}"
set :bind, $server_ip
set :port, $server_port
get '/get_presigned_url_for_oss_upload' do
url = get_presigned_url(client, object_key.to_s)
puts "Token: #{url}"
url
end
get '/*' do
puts "********************* GET "
send_file File.join(settings.public_folder, 'index.html')
end
C#
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using System.IO;
using System;
using Microsoft.Extensions.Logging;
using Aliyun.OSS;
namespace YourNamespace
{
public class Program
{
private ILogger<Program> _logger;
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_ID。
public string AccessKeyId { get; set; } = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID");
// 配置環(huán)境變量ALIBABA_CLOUD_ACCESS_KEY_SECRET。
public string AccessKeySecret { get; set; } = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
// <YOUR-ENDPOINT>替換為Bucket所在地域?qū)?yīng)的Endpoint。以華東1(杭州)為例,Endpoint填寫為https://oss-cn-hangzhou.aliyuncs.com。
private string EndPoint { get; set; } = "<YOUR-ENDPOINT>";
// 將<YOUR-BUCKET>替換為Bucket名稱。
private string BucketName { get; set; } = "<YOUR-BUCKET>";
private string ObjectName { get; set; } = "exampledir/exampleobject2.png";
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 添加日志
builder.Logging.AddConsole();
var logger = builder.Services.BuildServiceProvider().GetRequiredService<ILogger<Program>>();
app.UseStaticFiles(); // 添加這行以啟用靜態(tài)文件中間件
app.MapGet("/", async (context) =>
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "templates/index.html");
var htmlContent = await File.ReadAllTextAsync(filePath);
await context.Response.WriteAsync(htmlContent);
// 打印日志
logger.LogInformation("GET request to root path");
});
app.MapGet("/get_presigned_url_for_oss_upload", async (context) =>
{
var program = new Program(logger);
var signedUrl = program.GetSignedUrl();
logger.LogInformation($"SignedUrl: {signedUrl}"); // 打印token的值
await context.Response.WriteAsync(signedUrl);
});
app.Run();
}
// 構(gòu)造函數(shù)注入ILogger
public Program(ILogger<Program> logger)
{
_logger = logger;
}
private string GetSignedUrl()
{
// 創(chuàng)建OSSClient實(shí)例
var ossClient = new OssClient(EndPoint, AccessKeyId, AccessKeySecret);
// 生成簽名URL
var generatePresignedUriRequest = new GeneratePresignedUriRequest(BucketName, ObjectName, SignHttpMethod.Put)
{
Expiration = DateTime.Now.AddHours(1),
ContentType = "image/png"
};
var signedUrl = ossClient.GeneratePresignedUri(generatePresignedUriRequest);
return signedUrl.ToString();
}
}
}
Web端使用簽名URL上傳文件到OSS的示例代碼如下:
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const fileInput = document.querySelector("#file");
const file = fileInput.files[0];
fetch("/get_presigned_url_for_oss_upload", { method: "GET" })
.then((response) => {
if (!response.ok) {
throw new Error("獲取預(yù)簽名URL失敗");
}
return response.text();
})
.then((url) => {
fetch(url, {
method: "PUT",
headers: new Headers({
"Content-Type": "image/png",
}),
body: file,
}).then((response) => {
if (!response.ok) {
throw new Error("文件上傳到OSS失敗");
}
console.log(response);
alert("文件已上傳");
});
})
.catch((error) => {
console.error("發(fā)生錯(cuò)誤:", error);
alert(error.message);
});
});
客戶端直傳實(shí)踐參考
不同類型的客戶端的直傳實(shí)踐參考如下: