在客户端直接上传文件到OSS 您所在的位置:网站首页 阿里云oss请求价格 在客户端直接上传文件到OSS

在客户端直接上传文件到OSS

2024-07-05 10:46| 来源: 网络整理| 查看: 265

为什么客户端直传

在典型的服务端和客户端架构下,常见的文件上传方式是服务端代理上传:客户端将文件上传到业务服务器,然后业务服务器将文件上传到OSS。在这个过程中,一份数据需要在网络上传输两次,会造成网络资源的浪费,增加服务端的资源开销。为了解决这一问题,您可以在客户端直连OSS来完成文件上传,无需经过业务服务器中转。

image如何实现客户端直传

实现客户端直传需要解决以下两个大问题:

跨域访问

如果您的客户端是Web端或小程序,您需要解决跨域访问被限制的问题。浏览器以及小程序容器出于安全考虑,通常都会限制跨域访问,这一限制也会限制您的客户端代码直连OSS。您可以通过配置OSS Bucket的跨域访问规则,来允许指定域名下的Web应用或小程序直接访问OSS。更多信息,请参见跨域设置。

安全授权

上传文件到OSS需要使用RAM用户的访问密钥(AccessKey)来完成签名认证,但是在客户端中使用长期有效的访问密钥,可能会导致访问密钥泄露,进而引起安全问题。为了解决这一问题,您可以选择以下方案实现安全上传:

服务端生成STS临时访问凭证

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

服务端生成PostObject所需的签名和Post Policy

对于需要限制上传文件属性的场景,您可以在服务端生成PostObject所需的Post签名、PostPolicy等信息,然后客户端可以凭借这些信息,在一定的限制下不依赖OSS SDK直接上传文件。您可以借助服务端生成的PostPolicy限制客户端上传的文件,例如限制文件大小、文件类型。此方案适用于通过HTML表单上传的方式上传文件。需要注意的是,此方案不支持基于分片上传大文件、基于分片断点续传的场景。更多信息,请参见PostObject。

服务端生成PutObject所需的签名URL

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

服务端生成STS临时访问凭证

服务端通过STS临时访问凭证授权客户端上传文件到OSS的过程如下。

image

客户端向业务服务器请求临时访问凭证。

业务服务器使用STS SDK调用AssumeRole接口,获取临时访问凭证。

STS生成并返回临时访问凭证给业务服务器。

业务服务器返回临时访问凭证给客户端。

客户端使用OSS SDK通过该临时访问凭证上传文件到OSS。

OSS返回成功响应给客户端。

示例代码

以下示例代码为代码核心片段,如需查看完整代码请参考示例工程:sts.zip。

服务端示例代码

服务端生成临时访问凭证的示例代码如下:

说明

当前代码支持一键部署,您可以直接在函数计算FC中一键部署本代码。oss-upload-sts-app

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 # 将替换为拥有上传文件到指定OSS Bucket权限的RAM角色的ARN。 role_arn_for_oss_upload = '' # 将设置为STS服务的地域,例如cn-hangzhou。 region_id = '' def get_sts_token(): # 初始化 CredentialClient 时不指定参数,代表使用默认凭据链。 # 在本地运行程序时,可以通过环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID、ALIBABA_CLOUD_ACCESS_KEY_SECRET 指定 AK; # 在 ECS\ECI\容器服务上运行时,可以通过环境变量 ALIBABA_CLOUD_ECS_METADATA 来指定绑定的实例节点角色,SDK 会自动换取 STS 临时凭证。 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, # 将设置为自定义的会话名称,例如oss-role-session。 role_session_name='' ) response = sts_client.assume_role(assume_role_request) token = json.dumps(response.body.credentials.to_map()) return token 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) // 将设置为自定义的会话名称,例如 my-website-server。 .setRoleSessionName("") // 将替换为拥有上传文件到指定OSS Bucket权限的RAM角色的ARN,可以在 RAM 角色详情中获得角色 ARN。 RuntimeOptions runtime = new RuntimeOptions(); try { AssumeRoleResponse response = stsClient.assumeRoleWithOptions(assumeRoleRequest, runtime); return response.body.credentials; } catch (TeaException error) { // 如有需要,请打印 error assertAsString(error.message); return null; } catch (Exception error) { TeaException error = new TeaException(_error.getMessage(), _error); // 如有需要,请打印 error assertAsString(error.message); return null; } } } @Configuration public class StsClientConfiguration { @Bean public Client stsClient() { // 当您在初始化凭据客户端不传入任何参数时,Credentials工具会使用默认凭据链方式初始化客户端。 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; } }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初始化账号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 请参考 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) } const express = require("express"); const { STS } = require('ali-oss'); const app = express(); const path = require("path"); app.use(express.static(path.join(__dirname, "templates"))); // 配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID。 const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID; // 配置环境变量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填写自定义权限策略,用于进一步限制STS临时访问凭证的权限。如果不指定Policy,则返回的STS临时访问凭证默认拥有指定角色的所有权限。 // 3000为过期时间,单位为秒。 // sessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为sessiontest。 sts.assumeRole('', ``, '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"); }); require 'sinatra' require 'base64' require 'open-uri' require 'cgi' require 'openssl' require 'json' require 'sinatra/reloader' require 'sinatra/content_for' require 'aliyunsdkcore' # 设置public文件夹路径为当前文件夹下的templates文件夹 set :public_folder, File.dirname(__FILE__) + '/templates' def get_sts_token_for_oss_upload() client = RPCClient.new( # 配置环境变量ALIBABA_CLOUD_ACCESS_KEY_ID。 access_key_id: ENV['ALIBABA_CLOUD_ACCESS_KEY_ID'], # 配置环境变量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为过期时间,单位为秒。 "DurationSeconds": 3600, # sessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为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') endusing 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 _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(); 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(); // 将设置为自定义的会话名称,例如oss-role-session。 assumeRoleRequest.RoleSessionName = ""; // 将替换为拥有上传文件到指定OSS Bucket权限的RAM角色的ARN。 assumeRoleRequest.RoleArn = ""; 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 logger) { _logger = logger; } } }

客户端示例代码

Web端使用临时访问凭证上传文件到OSS的示例代码如下:

let credentials = null; const form = document.querySelector("form"); form.addEventListener("submit", async (event) => { event.preventDefault(); // 临时凭证过期时,才重新获取,减少对STS服务的调用。 if (isCredentialsExpired(credentials)) { const response = await fetch("/get_sts_token_for_oss_upload", { method: "GET", }); if (!response.ok) { // 处理错误的HTTP状态码。 throw new Error( `获取STS令牌失败: ${response.status} ${response.statusText}` ); } credentials = await response.json(); } const client = new OSS({ // 将设置为OSS Bucket名称。 bucket: "", // 将设置为OSS Bucket所在地域,例如region: 'oss-cn-hangzhou'。 region: "oss-", 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); }); /** * 判断临时凭证是否到期。 **/ function isCredentialsExpired(credentials) { if (!credentials) { return true; } const expireDate = new Date(credentials.Expiration); const now = new Date(); // 如果有效期不足一分钟,视为过期。 return expireDate.getTime() - now.getTime()


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有