news 2026/9/26 4:59:34

Python Flask 对接阿里云 STS:OSS 临时凭证安全上传方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python Flask 对接阿里云 STS:OSS 临时凭证安全上传方案

做后端的人迟早会碰到这个问题:业务要允许用户上传文件,文件存储在阿里云 OSS,但你不能把 AccessKey 直接暴露给前端或让文件绕过权限验证。我在工程里折腾过几轮之后,确定下来的标准方案就是 Python 后端对接阿里云 STS,签发出临时访问凭证,再通过 Flask 暴露 API,让内部系统拿着临时凭证把文件安全上传到 OSS。这篇文章会把这套方案的完整思路、核心代码、权限配置和排障过程全部讲清楚,适合正在搭文件上传服务、或者想把现有上传模块从"长期密钥裸奔"升级为"临时凭证"的开发者参考。

这套方案解决的核心痛点,概括起来就一句话:长期密钥不安全、不可控、到期收回成本高;而 STS 临时凭证天然有时效、有范围,就算被截获,也只是在窗口期里多了一个受限身份,不会让整个 Bucket 暴露在风险中。下面从设计思路开始,一步步把整条链路拆开讲透。

1. 为什么选 STS + Flask 这条路线

1.1 长期密钥的痛点和 STS 的定位

先说说我为什么不愿意用长期 AccessKey 直接做上传。长期凭证有两个绕不开的麻烦:不好收回,也不好精细控制范围。如果 AccessKey 泄露出去了,你得去控制台重新生成密钥,然后所有引用它的系统都要跟着改配置。就算你不把 AccessKey 暴露给前端,只让它躺在后端代码里,一旦代码仓库权限管理不严,密钥照样可能被"顺走"。

STS 做的事情,简单理解就是"用长期密钥换一张短期入场券"。这个入场券由阿里云 STS 服务签发,包含一组临时 AccessKeyId、AccessKeySecret 和 SecurityToken,默认有效期最短 15 分钟、最长 1 小时。过期之后自动作废,你不需要去吊销,也不需要通知所有调用方重新换密钥。调用方只要在这段时间内完成上传,之后凭证就自动失效——这就把"密钥轮换"这种事后补救动作,变成了"凭证过期"这种事前和事中兜底。

具体到 OSS 上传场景,STS 还有一个隐藏优势:它可以配合 RAM 角色做到非常细的授权。我会给角色配置一个类似"只能对 images 这个 Bucket 下面的 upload/ 目录执行 PutObject"的策略,就算前端 JS 文件里出现一段临时凭证,攻击者拿到的权限也仅限于往那个目录里写文件,看不了文件列表、删不了库、改不了别人的对象。这种精细程度是长期密钥很难做到的。

1.2 为什么服务端选 Flask 而不是其他框架

本质上,一个"签发临时凭证 + 代理上传"的服务,职责很小,不需要引入重型框架。用 Flask 的最直接原因是轻量:没有强制的 ORM、没有自带数据库迁移、没有复杂的工程骨架,几十行 Python 就能把接口跑起来。我之前用 Django 做过类似的内部工具,建项目、配 settings、注册 app 的流程比接口本身还费时间;换 Flask 之后,前后不到一个小时就立了一个可用的服务。

需要注意,我并不是让 Flask 承担所有业务逻辑。这层 API 只负责两件事:校验调用方身份、返回临时凭证或接收上传请求,然后把文件流转交给 OSS SDK。任何重计算、异步处理、格式转换都不要堆在这一层,否则 Flask 的轻量优势会被业务复杂度抵消。如果你的团队对 Python Web 生态没那么熟,这个选型依然是门槛最低的。

1.3 整体架构:先分清两种上传模式

后面代码里会反复涉及两个概念,这里必须先讲清楚。

第一种是"服务端代传"。客户端把文件 POST 到 Flask API,Flask 拿到文件流,自己再去调用 STS 获取临时凭证,然后以临时凭证身份把文件写入 OSS,最后返回一个可访问的 URL。这种模式适合内部系统、文件不大、并发量可控的场景,开发上最简单,但也意味着服务器要承接文件的中转流量。

第二种是"客户端直传"。Flask 只负责签发并返回 STS 临时凭证,前端拿到凭证后直接用阿里云 OSS 的 JS SDK 把文件传上去。文件不经过应用服务器,带宽压力全部落到了 OSS 的接入层,支持大文件分片上传,适合面向公众用户的高并发产品。

这两条路看着不同,底层完全一样,都依赖 STS 的 AssumeRole 流程。先把 STS 和权限这一层打通,选哪种模式就是切换接口形态的问题而已。

2. 动手前的硬准备:RAM 角色与权限策略

2.1 创建 RAM 角色并配置信任策略

整个流程的起点是创建一个 RAM 角色。登录阿里云 RAM 控制台,选择"角色管理",创建一个"用户自定义"角色,可信实体选"阿里云账号"。这里最关键的一项配置是信任策略,它规定"谁有资格扮演这个角色"。我的策略大概长这样:

{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": "sts:AssumeRole", "Principal": { "RAM": ["acs:ram::<阿里云账号UID>:root"] } } ] }

如果生产环境里有多个 RAM 用户,可以把 Principal 从 root 收得更紧,改成具体的用户,例如 acs:ram:: :user/oss-upload-bot。我这边用 root 写法的原因,是实际持有长期密钥的 RAM 用户本身就是专用的运维账号,已经和其他账号做了隔离;但是如果你团队里账号划分不清晰,建议还是不要把信任范围放宽到 root。

2.2 角色授权策略:按最小权限设计

给角色附加权限策略时,最容易犯的错是顺手放一个"AliyunOSSFullAccess"。这份策略确实能让所有功能跑通,但它也意味着扮演成功的临时凭证能对名下所有 Bucket 做任何操作,包括删除、列举、修改权限。安全起见,我每次都会按最小权限写策略:所有上传都落到同一个目录下,策略里只授权这个目录的写入和读取:

{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": ["oss:PutObject"], "Resource": [ "acs:oss:*:*:my-bucket/upload/*" ] } ] }

如果之后还需要生成下载链接,再补一条 oss:GetObject;需要列出文件目录,再补 oss:ListObjects。我的建议是第一次实现只放 PutObject,等某个具体需求确实出现,再逐条追加权限。权限这东西,放出去容易,哪天想收回来,运行中的老旧业务十有八九会"炸",所以起步越窄越好。

2.3 准备环境:依赖安装与配置项

代码层面依赖三个库:Flask、oss2、阿里云 STS SDK。直接在虚拟环境里安装:

pip install flask oss2 aliyun-python-sdk-sts

这里提醒一句,aliyun-python-sdk-sts 对 aliyun-python-sdk-core 的版本有依赖,如果你之前装过旧版 core,建议一起升级,避免出现循环依赖或者方法签名不匹配。Python 版本我建议 3.10 及以上,倒不是说低版本跑不了,而是新版对类型标注和异步支持更好,后面扩展起来舒服。

配置项不要写死在源码里。我统一放到环境变量,部署时直接通过进程环境注入:

export ALIBABA_CLOUD_ACCESS_KEY_ID="LTAI5t****" export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-long-term-secret" export STS_ROLE_ARN="acs:ram::<UID>:role/oss-uploader" export OSS_BUCKET_NAME="my-bucket" export OSS_ENDPOINT="oss-cn-hangzhou.aliyuncs.com" export OSS_BASE_DIR="upload"

3. Flask API 核心实现:临时凭证签发与文件上传

3.1 签发 STS 临时凭证的完整代码

先写一个独立的模块,负责调用 AssumeRole。我一般单独建一个 sts_client.py,方便后续其他服务复用:

import json from aliyunsdkcore.client import AcsClient from aliyunsdksts.request.v20150401 import AssumeRoleRequest def get_assume_role_token( access_key_id: str, access_key_secret: str, role_arn: str, session_name: str = "flask-app", duration_seconds: int = 3600, region: str = "cn-hangzhou", ) -> dict: client = AcsClient(access_key_id, access_key_secret, region) request = AssumeRoleRequest.AssumeRoleRequest() request.set_RoleArn(role_arn) request.set_RoleSessionName(session_name) request.set_DurationSeconds(duration_seconds) response = client.do_action_with_exception(request) result = json.loads(response) return result["Credentials"]

返回结果里就是经典的四个字段:AccessKeyId、AccessKeySecret、SecurityToken、Expiration。有一点值得明确:这个临时 AccessKeyId 的典型前缀是 STS.,和我们长期密钥的格式完全不同。看到这个前缀,就应该意识到当前的身份是扮演出来的临时身份,只能用于限定时长的操作。

3.2 用临时凭证完成 OSS 上传

Flask 侧的服务端代传模式,核心实现如下:

import os import uuid import time from flask import Flask, request, jsonify import oss2 app = Flask(__name__) ACCESS_KEY_ID = os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"] ACCESS_KEY_SECRET = os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"] STS_ROLE_ARN = os.environ["STS_ROLE_ARN"] OSS_BUCKET_NAME = os.environ["OSS_BUCKET_NAME"] OSS_ENDPOINT = os.environ.get("OSS_ENDPOINT", "oss-cn-hangzhou.aliyuncs.com") OSS_BASE_DIR = os.environ.get("OSS_BASE_DIR", "upload") # 全局简单缓存,避免每次都重新调用 STS _global_cache = {} def _refresh_token_if_needed(): now = time.time() if "expire_at" not in _global_cache or now >= _global_cache["expire_at"]: cred = get_assume_role_token( ACCESS_KEY_ID, ACCESS_KEY_SECRET, STS_ROLE_ARN, session_name=f"flask-app-{int(now)}", duration_seconds=3600, ) _global_cache["cred"] = cred _global_cache["expire_at"] = iso_to_timestamp(cred["Expiration"]) - 300 return _global_cache["cred"] @app.post("/api/oss/upload") def upload_file(): if "file" not in request.files: return jsonify({"error": "file is required"}), 400 file = request.files["file"] # 通过 uuid 隔离多人同名文件 key = f"{OSS_BASE_DIR}/{uuid.uuid4().hex}/{file.filename}" cred = _refresh_token_if_needed() auth = oss2.StsAuth( cred["AccessKeyId"], cred["AccessKeySecret"], cred["SecurityToken"], ) bucket = oss2.Bucket(auth, OSS_ENDPOINT, OSS_BUCKET_NAME) bucket.put_object(key, file.stream) url = f"https://{OSS_BUCKET_NAME}.{OSS_ENDPOINT}/{key}" return jsonify({"key": key, "url": url}), 200

这段代码里最关键的一行是 oss2.StsAuth。oss2 的 Bucket 构造支持三种认证方式:Auth(长期密钥)、StsAuth(临时凭证)、AnonymousAuth(匿名访问)。使用临时凭证时,SecurityToken 必须作为第三个参数传进去,漏传的话 OSS 端签名校验直接失败,返回 AccessDenied。

至于在对象 key 中插入 uuid 片段,是为了避免不同用户上传同名文件相互覆盖。只要你还在做多用户上传,这个习惯一定要养成。后面要做日志追溯时,uuid 也是一条很高效的索引线索。

3.3 关键参数解析:DurationSeconds 与 RoleSessionName

DurationSeconds 的边界要记清楚:最小值 900,也就是 15 分钟;最大值默认 3600,也就是 1 小时。如果角色配置了更短的最大会话时间,参数再大也没有用,实际有效期取两者的较小值。

我默认用的是 3600,但从安全角度讲,纯上传型接口其实用不着 1 小时。前端传到后端、后端写 OSS,整个过程在正常网络下就是秒级到分钟级的事,所以我更推荐把 DurationSeconds 缩短到 900 到 1800 之间。凭证越短,被截获后的破坏窗口越小。

RoleSessionName 的作用很多人会忽略。在云审计的日志里,每次 AssumeRole 都会被记录,RoleSessionName 就是用来区分具体发起方的标签。我会让每个用户传入一个唯一会话名,例如 user_123 或 order_20250101_001,后续出了问题,通过日志能直接定位是哪一次调用、哪个用户、上传了哪些对象。这个字段只能包含字母、数字和.@-_等字符,别放中文或者超长字符串。

4. 直传模式:前端拿临时凭证直接传 OSS

4.1 直传解决什么问题

如果你做的是面向公众用户的产品,我强烈建议用直传模式。原因很简单:服务端代传时,用户上传一个 100MB 的视频,你的 ECS 要先收 100MB,再转发 100MB 到 OSS,服务器带宽被硬生生吞掉一份中转流量。高峰期并发上来,网络 I/O 和 CPU 都容易被打满,服务器成了瓶颈,OSS 却闲着。

直传模式下,Flask 只负责下发临时凭证,之后浏览器和 OSS 之间直接建立上传链路。文件内容不经过应用服务器,带宽压力全部落在 OSS 上,应用服务器只需要处理一个轻量的凭证请求。如果你的应用有图片压缩、视频转码这类需求,可以等直传完成后再触发异步处理任务,而不是让上传本身阻塞在应用层。

4.2 后端签发凭证接口

直传模式下 Flask 接口变得更简单,它不再接收文件,只返回凭证:

@app.post("/api/sts/token") def issue_sts_token(): # 实际生产环境里,这里必须校验调用方身份,比如 Header API Key cred = get_assume_role_token( ACCESS_KEY_ID, ACCESS_KEY_SECRET, STS_ROLE_ARN, session_name=f"user-{request.remote_addr}", duration_seconds=900, ) return jsonify({ "access_key_id": cred["AccessKeyId"], "access_key_secret": cred["AccessKeySecret"], "security_token": cred["SecurityToken"], "expiration": cred["Expiration"], "bucket": OSS_BUCKET_NAME, "endpoint": OSS_ENDPOINT, })

注意这里 DurationSeconds 我直接给了 900 秒。原因是浏览器从拉取凭证到完成上传,通常只有几十秒,完全没有必要让凭证活得更久。另外,session_name 这里我偷懒用了 IP,但真实生产环境更合理的做法是拼接用户 ID 和会话 ID,因为同一个出口 IP 可能对应多个用户,会降低审计日志的准确性。

4.3 前端配合与 CORS 配置

前端拿到凭证后,直接用阿里云官方 SDK 或者简单 fetch 写文件:

const resp = await fetch("/api/sts/token", { method: "POST" }); const data = await resp.json(); const client = new OSS({ region: "oss-cn-hangzhou", accessKeyId: data.access_key_id, accessKeySecret: data.access_key_secret, stsToken: data.security_token, bucket: data.bucket }); const result = await client.put("upload/example.png", file); console.log(result.url);

浏览器直传 OSS 会遇到跨域问题。OSS Bucket 的"跨域设置"里必须增加一条允许来源为你的域名,Allowed 方法至少要包含 PUT、POST、GET、HEAD,还要允许 OPTIONS 预检请求,因为 OSS SDK 在上传前会先发一个 OPTIONS 请求确认服务器允许跨域。我第一次在线上配置时就把 Allowed Header 漏了,前端一直报 CORS 错误,排查半天才发现是预检请求被拦。

提示:CORS 配置里的 Allowed Origin,生产环境尽量不要用*。精确到你的业务域名,能避免其他已知站点借用你 Bucket 的跨域能力。本地调试时再用通配符或者 localhost 单独开一条规则。

5. 踩坑实录与排查技巧

5.1 常见错误码速查表

我把自己遇到过的和身边同事踩过的典型报错整理成一个速查表,遇到问题先按这个思路过一遍。

错误码 / 报错信息出现场景排查方向
InvalidAccessKeyId.NotFound使用了 STS 返回的临时 AccessKeyId,但请求里没带 SecurityToken检查 oss2.StsAuth 是否传入第三个参数
AccessDenied临时凭证没有对应目录的写权限检查 RAM 角色授权策略的 Resource 是否正确
SecurityTokenExpired凭证超过有效期重新调用 AssumeRole,并实现提前刷新的缓存逻辑
InvalidParameterDurationSeconds 超范围,或 RoleArn 格式错误检查 900 ≤ duration ≤ 3600,RoleArn 前缀必须是 acs:ram::
SignatureDoesNotMatch服务器时钟偏移,或 Secret 抄错校准系统时间,确认长期 AccessKeySecret 和角色 ARN 无误
BucketAlreadyExists创建 bucket 时名称被占用bucket 全局唯一,换名字或使用已有 bucket

5.2 我在实际工程中踩过的三个坑

第一个坑是临时凭证缓存策略没做好。我第一次实现时,把凭证缓存写成了"进程启动时获取一次,之后一直复用",结果跑了一个小时左右上传开始大面积报 SecurityTokenExpired。改成每次上传都重新调用 STS 后,又发现高并发下 STS 接口调用量太大。最终方案就是在内存里缓存凭证,并且在真正过期前 5 分钟提前刷新。这样 STS 的调用频率被压到瓶颈以下,上传链路又不中断。

第二个坑是中文文件名的编码问题。OSS 控制台里看文件名没问题,但通过浏览器访问 URL 时出现乱码。排查后确认是 key 里直接包含中文和空格,链路各层对 URL 的编码处理不一致。后来我统一改成服务端生成 uuid 文件名作为 OSS 对象 key,原始文件名在 API 的响应字段里单独返回,展示层自己去解码,彻底避开 URL 编码的坑。

第三个坑是前端直传模式下的 CORS 预检。本地开发环境用 localhost:5000 去调 OSS 没问题,一换到线上域名就报跨域。检查发现 OSS 控制台的跨域规则里只配了 GET,没配 OPTIONS,也没有把实际的请求头加入 Allowed Header。把跨域规则补齐后,问题当场解决。还有一个容易忽略的细节:Bucket 的跨域规则是分区域生效的,如果你用了多个 Region,每个 Bucket 都要单独配。

6. 生产环境的安全加固建议

6.1 密钥管理:环境变量只是一个起点

环境变量解决了"密钥不写死在代码里"的问题,但它仍然意味着运维能直接读到明文。更稳妥的方案是把密钥放到密钥管理类服务中,比如阿里云 KMS,或者外部 Vault,应用启动时再动态拉取。我见过不少本可以避免的事故,背后都是同一个原因:AccessKeySecret 被写进配置文件并推到 Git 仓库,后来仓库权限失控,密钥跟着泄漏。

我自己的习惯是:长期密钥只存在于部署环境的内存中,任何日志、响应体、数据库字段都不能出现完整的 AccessKeySecret。前端哪怕只瞥到一眼,都应该立即轮换。如果你发现日志里有 SDL 工具扫出来的密钥字样,宁愿多花 5 分钟重新生成,也不要侥幸它只是一次性打印。

6.2 签发临时凭证的接口必须加鉴权

这一点再怎么强调都不过分:开放一个没有任何校验的 STS 签发接口,等于把 RAM 角色的完整能力变成公共 API。只要有人拿到你的接口地址,他就能不停地换取临时凭证,然后利用角色权限往 OSS 里传垃圾文件,甚至制造流量账单。

最简单的做法是给调用方分配 API Key,放在请求头里校验。如果是面向普通用户的网页,则要求登录态,并且在签发时绑定用户 ID、限制来源 IP。理论上还应给每个账号设置 STS 签发频率上限,防止被恶意利用者循环调用。

6.3 凭证时效、目录隔离与审计闭环

在实际部署里,我会把"签发凭证"和"文件上传"拆成两个独立服务。Flask 只负责前者,后者沉淀成一个公共上传组件,供不同业务复用。这样每个业务可以有自己的角色和目录,即使某个业务线被攻击,损失也不会横向扩散到其他业务线。

策略上,建议每个模块分配一个独立的前缀目录,比如 upload/order/、upload/user/avatar/,然后在策略 Resource 里分别匹配。临时凭证的时长按业务类型设:上传头像这类小文件 15 分钟足够,而视频转码这类大对象上传,可以适当放宽到 30 分钟,但最好不要超过 1 小时。

最后,开启云审计的 STS 调用记录。每天花一分钟扫一眼 AssumeRole 日志,重点看来源 IP 和 RoleSessionName 是否和业务预期一致。发现异常调用,立即禁用对应 RAM 用户或收紧角色的信任策略,同时轮换长期密钥。这套闭环跑顺之后,文件上传这条链路的安全性就基本可控了。

按照我个人的习惯,在一开始就预留好"凭证签发接口和文件上传组件分离"的扩展点,后面迁移新业务非常省事。回看整个实现,最值得记住的一点其实是:STS 并不是把技术方案变复杂,而是把安全从"密钥轮换"这种事后补救动作,前移成了"凭证过期"这种事前和事中兜底。如果你现在还在用长期 AccessKey 做上传服务,下次改版时优先考虑迁移到这套方案,哪怕只是把凭证有效期调短一点,整体的攻击暴露面也已经缩小了一个量级。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 4:59:34

VMware Workstation故障排查:从Hyper-V冲突到vcpu异常

VMware Workstation 用了十几年&#xff0c;遇到过的故障五花八门&#xff0c;但把日志翻出来一看&#xff0c;十有八九问题都出在同几个地方——Hyper-V残留、服务被禁用、vmx文件配置被改坏、安装包没下载全。写这么一篇VMware Workstation 常见故障排查指南&#xff0c;不是…

作者头像 李华
网站建设 2026/9/26 4:58:48

OpenMontage:本地部署的视频编辑Agent实战指南

1. 这不是“AI剪视频”&#xff0c;而是第一次看到AI Agent真正接管整条视频生产流水线最近在几个技术群里被反复问到一个问题&#xff1a;“OpenMontage到底能不能自己做完一条视频&#xff1f;”——注意&#xff0c;这里说的“做完”&#xff0c;不是指把几段素材拖进时间线…

作者头像 李华
网站建设 2026/9/26 4:58:48

代码100%开源! 一款开源免费的匿名在线即时聊天(IM)系统

&#x1f482; 个人网站: IT知识小屋&#x1f91f; 版权: 本文由【IT学习日记】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 文章目录简介架构功能列表功能截图开源地址&使用手册写在最后简介 AQ…

作者头像 李华
网站建设 2026/9/26 4:57:29

Flutter鸿蒙跨平台倒计时秒表开发实战与性能优化

1. 项目概述&#xff1a;当Flutter遇上鸿蒙&#xff0c;做一个真正好用的计时工具先聊一个很多做跨平台开发的同行最近都在纠结的事——鸿蒙生态起来了&#xff0c;但要不要单独维护一套原生代码&#xff1f;我的答案是&#xff1a;不一定。这篇博文要聊的项目&#xff0c;就是…

作者头像 李华
网站建设 2026/9/26 4:57:16

编译原理中的正则表达式与正则定义:词法分析核心解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华