客户端签名直传是指从服务端获取sts token,并使用sts token生成签名,最后使用此签名上传文件到OSS。客户端可以直接将文件上传至OSS,减少了中间环节,因此可以加快上传速度。本文介绍如何进行客户端签名直传。
安全风险
为了减少企业的业务服务器负担并提高效率,该Web应用被设计为在用户的网页浏览器中直接与OSS交互,而非所有请求都通过企业的业务服务器中转。
企业A计划采取以下方案搭建Web应用文件直传服务:
由于网页浏览器运行环境完全处于用户端且不受企业A直接控制,企业A面临以下挑战:
密钥泄露的重大风险:网页浏览器归用户所有,并且被视为不可信的环境。若在浏览器中存储RAM用户的访问密钥等敏感信息,则可能会面临密钥意外泄露的显著风险。一旦密钥落入不法分子之手,他们就可能利用这些密钥非法访问企业在云端的敏感数据,导致核心商业机密、客户隐私信息、知识产权及其他关键资产的泄漏,给企业造成不可估量的经济损失和信誉损害。
过度权限的安全漏洞:在为企业的业务服务器应用程序创建RAM用户时,往往需要赋予该用户较高的权限,以便于它可以访问和管理其他云服务。然而,如果用户的访问密钥被存储在浏览器中而后又意外泄露,这将可能导致拥有过多权限的密钥落入恶意攻击者之手。此举不仅增加了企业资源被滥用的概率,还可能带来更严重的问题。黑客可能会使用盗取的密钥在企业的云资源上部署病毒或恶意软件,比如植入后门、植入密码窃取木马程序、以企业资源进行加密货币挖矿等,严重破坏企业的正常业务运营,并对企业的财务和声誉构成严重威胁。
解决方案
为了应对上述风险,企业A可以在原有方案的基础上增加临时授权。通过这种方式,企业A能够在确保数据直传效率的同时,实现以下效果:
增强的身份验证和授权:通过STS生成的具有时间限制的令牌,即便在短时间内泄露,也极大降低了安全风险。因为这些凭证很快就会失效,降低了被不当利用的可能性。
精细化权限控制:STS允许根据最小权限原则配置权限,仅授权Web应用必需的访问权限。这种精细化的权限控制方法限制了潜在泄露的影响范围,防止了过度权限的风险。
企业A最终采取以下方案搭建Web应用文件直传服务:
方案部署
下面将以一个简单的用户文件上传场景为例,引导您一步步使用OSS和STS为Web应用部署浏览器文件直传服务。
本方案以python代码为例,部署的示例工程:oss-upload-sts.zip
本方案使用的ROS模板:add-signatures-on-the-client-by-using-JS-and-upload-data.yml
获取临时STS身份凭证的Java代码,可参考:ststoken.zip
一键部署
您可以一键部署通过ROS快速完成客户端签名直传,无需手动执行以下操作步骤。
手动部署
准备工作
创建一个OSS Bucket,用于存储Web应用在浏览器环境中直接上传的文件。
参数
示例值
所属地域
华东1(杭州)
Bucket名称
web-direct-upload
具体步骤,请参见创建存储空间。
创建一台ECS实例作为业务服务器,用于生成临时身份凭证。
说明在实际部署时,您可以将调用STS服务的接口集成到自己的业务服务器的接口中,而无需创建该ECS实例。
参数
示例值
付费类型
按量付费
地域
华东1(杭州)
公网 IP
分配公网 IPv4 地址
安全组
开放HTTP (TCP:80)端口
具体步骤,请参见通过控制台使用ECS实例(快捷版)。
为创建的OSS Bucket配置跨域资源共享。
参数
示例值
来源
http://ECS公网IP地址
允许Methods
PUT
允许Headers
*
具体步骤,请参见跨域设置。
部署步骤
步骤一:在访问控制创建RAM用户
首先,创建一个调用方式为OpenAPI调用的RAM用户,并获取对应的访问密钥,作为业务服务器的应用程序的长期身份凭证。
使用云账号或账号管理员登录RAM控制台。
在左侧导航栏,选择身份管理 > 用户。
单击创建用户。
输入登录名称和显示名称。
在调用方式区域下,选择OpenAPI调用,然后单击确定。
单击操作下的复制,保存调用密钥(AccessKey ID和AccessKey Secret)。
步骤二:在访问控制为RAM用户授予调用AssumeRole接口的权限
创建RAM用户后,需要授予RAM用户调用STS服务的AssumeRole接口的权限,使其可以通过扮演RAM角色来获取临时身份凭证。
在左侧导航栏,选择身份管理 > 用户。
在用户页面,找到目标RAM用户,然后单击RAM用户右侧的添加权限。
在新增授权页面,选择AliyunSTSAssumeRoleAccess系统策略。
说明授予RAM用户调用STS服务AssumeRole接口的固定权限是AliyunSTSAssumeRoleAccess,与后续获取临时访问凭证以及通过临时访问凭证发起OSS请求所需权限无关。
单击确认新增授权。
步骤三:在访问控制创建RAM角色
为当前云账号创建一个RAM角色,并获取对应的角色的ARN(Aliyun Resource Name,阿里云资源名称),用于RAM用户之后进行扮演。
在左侧导航栏,选择身份管理 > 角色。
单击创建角色,可信实体类型选择阿里云账号,单击下一步。
填写角色名称,选择当前云账号。
单击完成。完成角色创建后,单击关闭。
在RAM角色管理页面,搜索框输入角色名称,例如
oss-web-upload
。单击复制,保存角色的ARN。
步骤四:在访问控制创建自定义权限策略
按照最小授权原则,为RAM角色创建一个自定义权限策略,限制只能向指定OSS的存储空间进行XX操作。
在左侧导航栏,选择权限管理 > 权限策略。
单击创建权限策略。
在创建权限策略页面,单击脚本编辑,将以下脚本中的
<Bucket名称>
替换为准备工作中创建的Bucket名称。重要以下示例仅供参考。您需要根据实际需求配置更细粒度的授权策略,防止出现权限过大的风险。关于更细粒度的授权策略配置详情,请参见对象存储自定义权限策略参考。
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": "oss:PutObject", "Resource": "acs:oss:*:*:<Bucket名称>/uploads/*" } ] }
策略配置完成后,单击继续编辑基本信息。
在基本信息区域,填写策略名称,然后单击确定。
步骤五:在访问控制为RAM角色授予权限
为RAM角色授予创建的自定义权限,以便该RAM角色被扮演时能获取所需的权限。
在左侧导航栏,选择身份管理 > 角色。
在角色页面,找到目标RAM角色,然后单击RAM角色右侧的新增授权。
在新增授权页面下的自定义策略页签,选择已创建的自定义权限策略。
单击确定。
步骤六:在业务服务器获取临时身份凭证
在Web应用中,通过在业务服务器集成STS SDK,实现一个获取临时STS身份凭证的接口。当这个接口(/get_sts_token
)通过HTTP GET方法被访问时,它会生成一个临时身份凭证,并将其返回给请求者。
在ECS实例上,使用Flask框架快速搭建Web应用,实现一个获取临时STS身份凭证的接口的操作示例如下:
连接ECS实例。
具体操作,请参见通过控制台使用ECS实例(快捷版)。
安装Python3。
创建项目文件夹,然后切换到项目目录。
mkdir my_web_sample cd my_web_sample
安装依赖。
pip3 install Flask pip3 install attr pip3 install yarl pip3 install async_timeout pip3 install idna_ssl pip3 install attrs pip3 install aiosignal pip3 install charset_normalizer pip3 install alibabacloud_tea_openapi pip3 install alibabacloud_sts20150401 pip3 install alibabacloud_credentials
编写后端代码。
创建一个
main.py
文件。在这个文件中,添加以下Python代码。
import json from flask import Flask, render_template 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 app = Flask(__name__) # 将<YOUR_ROLE_ARN>替换为RAM角色的ARN。 role_arn_for_oss_upload = '<YOUR_ROLE_ARN>' # 设置为STS服务的地域,例如cn-hangzhou。 region_id = 'cn-hangzhou' @app.route("/") def hello_world(): return render_template('index.html') @app.route('/get_sts_token', methods=['GET']) 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, # 将<YOUR_ROLE_SESSION_NAME>设置为自定义的会话名称。 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 app.run(host="0.0.0.0", port=80)
将代码中的
<YOUR_ROLE_ARN>
替换为步骤三获取的角色ARN。将代码中的
<YOUR_ROLE_SESSION_NAME>
设置为自定义的会话名称,例如role_session_test
。
使用步骤一获取的访问密钥启动应用程序。
ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
在浏览器中访问
http://<ECS实例公网IP地址>/get_sts_token
。成功返回示例如下:
按
Ctrl + C
停止应用程序。
步骤七:在浏览器使用临时身份凭证上传文件到OSS
在业务服务器配置了获取STS临时身份凭证的接口后,在Web应用的前端网页使用CDN引入OSS JavaScript SDK,实现对文件上传的监听。当用户上传文件,调用/get_sts_token
接口从业务服务器请求临时访问凭证,然后使用临时访问凭证向OSS上传文件。
在ECS上,使用CDN引入方式将OSS JavaScript SDK集成到Web应用的前端代码的操作示例如下
按
Ctrl + C
停止应用程序。创建前端项目文件。
mkdir templates
创建HTML模板文件。
在
templates
目录中创建一个index.html
文件。vim templates/index.html
在这个文件中,添加以下HTML代码。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>上传文件到OSS</title> <script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.17.0.min.js"></script> </head> <body> <div class="container"> <form> <div class="mb-3"> <label for="file" class="form-label">选择文件</label> <input type="file" class="form-control" id="file" name="file" required /> </div> <button type="submit" class="btn btn-primary">上传</button> </form> </div> <script type="text/javascript"> 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", { method: "GET", }); if (!response.ok) { // 处理错误的HTTP状态码 throw new Error( `获取STS令牌失败: ${response.status} ${response.statusText}` ); } credentials = await response.json(); } const client = new OSS({ // 将<YOUR_BUCKET>设置为OSS Bucket名称。 bucket: "<YOUR_BUCKET>", // 将<YOUR_REGION>设置为OSS Bucket所在地域,例如oss-cn-hangzhou。 region: "<YOUR_REGION>", // 如果这里使用的是本地获取的ststoken的参数,请将credentials.xxx手动替换为已获取到参数的具体数值 accessKeyId: credentials.AccessKeyId, accessKeySecret: credentials.AccessKeySecret, stsToken: credentials.SecurityToken, }); const fileInput = document.querySelector("#file"); const file = fileInput.files[0]; const result = await client.put('uploads/'+file.name, file); console.log(result); }); /** * 判断临时凭证是否到期。 **/ function isCredentialsExpired(credentials) { if (!credentials) { return true; } // 如果这里使用的是本地获取的ststoken的参数,请将credentials.Expiration手动替换为已获取到参数的具体数值 const expireDate = new Date(credentials.Expiration); const now = new Date(); // 如果有效期不足一分钟,视为过期。 return expireDate.getTime() - now.getTime() <= 60000; } </script> </body> </html>
将代码中的
<YOUR_BUCKET
和<YOUR_REGION>
替换为准备工作中创建的Bucket名称和Bucket所属地域ID。关于地域ID,请参见OSS访问域名、数据中心、开放端口。
使用步骤一获取的访问密钥启动应用程序。
ALIBABA_CLOUD_ACCESS_KEY_ID=<YOUR_AK_ID> ALIBABA_CLOUD_ACCESS_KEY_SECRET=<YOUR_AK_SECRET> python3 main.py
在浏览器中访问
http://<ECS实例公网IP地址>
,然后在页面中选择并上传文件,模拟真实用户在浏览器的行为。
完成与清理
方案验证
完成以上操作后,您可以查看文件是否已上传到OSS。
在左侧导航栏,选择Bucket列表。
在文件列表页面,查看成功上传的文件。
清理资源
在本方案中,您创建了1台ECS实例、1个OSS Bucket、1个RAM用户和1个RAM角色。测试完方案后,您可以参考以下规则处理对应产品的资源,避免继续产生费用或产生安全风险。
后续操作
终端用户在浏览器上传文件到OSS后:
您可以为文件生成签名URL,授权用户在指定时间内下载或者预览文件。具体操作,请参见使用文件URL分享文件。
您可以对图片进行处理,例如添加图片水印、转换格式等。具体操作,请参见图片处理。