1. 为什么不用 AccessKey 直传,而是折腾一套 STS
先说结论:如果你的 Flask 服务端只负责把文件上传到 OSS,并且这个服务只给你自己的后台用,那直接用阿里云主账号或者 RAM 用户的 AccessKey 写死在代码里,确实能跑,而且代码量至少少三分之一。但只要是涉及到前端直传、第三方用户上传、或者你的服务要暴露在公网环境,AccessKey 直传就是一颗定时炸弹。
我见过不少团队把 AccessKey 放在前端 JavaScript 代码里,或者放在 Flask 的配置文件里提交到 Git 仓库,结果就是某天云账号被刷了几千块流量账单。AccessKey 等同于你云资源的全部管理权限,一旦泄露,对方能做的事远不止上传文件——删 Bucket、开新实例、拉取你所有的数据快照,全都可以。而 STS(Security Token Service)临时凭证的核心价值就在于:它是临时的,有效期可以设置成 15 分钟到 12 小时;它是受限的,可以通过 RAM 角色授权精确到某个 Bucket 的某个目录;它是可回收的,即使泄露,过期即失效。
所以这套方案的典型适用场景是:
- 前端网页或 App 需要直接上传文件到 OSS,不想经过 Flask 服务端中转,减少服务器带宽和内存压力
- 你给第三方开放上传能力,但不希望暴露你的长期密钥
- 多环境隔离,测试环境和生产环境使用不同的 RAM 角色,互不干扰
- 审计需求,所有临时凭证的颁发和使用都可以在云盾和 RAM 操作日志中追踪
这个标题里的技术组合,其实就是把 STS 临时凭证的获取封装在 Flask API 里,客户端请求这个接口拿到临时凭证后,直接用 OSS SDK 上传。整个链路是:客户端 → Flask API(STS 接口)→ RAM 角色(临时凭证)→ OSS 文件上传。下面我把每一步拆开讲清楚,并附上可直接复现的完整代码。
2. 核心机制拆解:STS、RAM 角色和 OSS 是怎么协作的
2.1 STS 临时凭证的三个关键字段
你调用 STS 接口后,返回的凭证包含三个核心部分,缺一不可:
- AccessKeyId:临时凭证的 ID,形式上看起来和长期 AccessKey 一致,但前面通常带一个前缀区分
- AccessKeySecret:临时凭证的密钥,配合上面的 ID 使用
- SecurityToken:这是 STS 独有的令牌字段,OSS SDK 在初始化客户端时必须指定,否则会报 InvalidAccessKeyId 错误
实际使用中,SecurityToken 是最容易被忽略的。很多人用长期密钥的习惯没改过来,拿着临时 AccessKeyId 和 AccessKeySecret 就往 OSS SDK 里填,结果怎么调试都报权限错误。我第一次踩这个坑的时候,排查了整整两个小时,最后发现就是少传了 SecurityToken。
这三个字段的有效期由你在调用 AssumeRole 接口时通过 DurationSeconds 参数指定,范围是 900 秒(15 分钟)到 43200 秒(12 小时)。不建议设置得太长,因为临时凭证的作用就是缩小风险窗口。如果某个上传任务超过 12 小时,应该重新获取凭证而不是拉长有效期。
2.2 RAM 角色和授权策略的作用
RAM 角色是一个虚拟身份,它本身没有长期凭证,只能通过 AssumeRole 来换取临时凭证。你在创建角色时必须附加一个信任策略(Trust Policy),这个策略声明了谁有权限来 Assume 这个角色——通常就是你指定的一批 RAM 用户。
角色创建好后,还要给它附加一个权限策略(Permission Policy),这个策略决定了临时凭证能做什么操作。我们做 OSS 上传,最小权限只需要类似这样的配置:
{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": [ "oss:PutObject", "oss:GetObject" ], "Resource": [ "acs:oss:*:*:your-bucket-name/uploads/*" ] } ] }注意 Resource 字段里的路径限定,我写了 uploads/*,意味着这个角色只能操作该 Bucket 下 uploads 目录里的对象,其他的读不了也写不了。这就是最小权限原则的落地:临时凭证即使被拿到,攻击者也最多往你指定的目录里塞文件,动不了其他任何数据。
2.3 客户端上传的两种路径对比
STS 可以配合两种上传模式,这个要提前想清楚,因为它直接决定了你的 Flask API 设计:
第一种:服务端中转上传。客户端把文件交给你的 Flask API,你的 Flask 服务拿到临时凭证后,自己调用 OSS SDK 上传。好处是逻辑最简单,所有上传行为都经过你的服务,方便做二次鉴权和审计;坏处是文件流经你的服务器,占用带宽和处理时间,大文件上传时 Flask 服务可能成为瓶颈。
第二种:客户端直传。Flask API 只负责返回临时凭证,客户端拿到凭证后用 OSS SDK 直连 OSS 上传,不经过你的服务器。好处是 OSS 有多线接入和断点续传能力,大文件上传体验更好,服务器压力小;坏处是上传逻辑要写在客户端,而且你要信任客户端不会滥用临时凭证(不过权限策略已经把可操作范围限定死了,风险可控)。
我在项目里两种方案都用了,一般建议优先考虑第二种,这也是标题里"Flask API 实现"的核心价值——你的 Flask 服务做的事情是颁发临时凭证,而不是搬运文件数据。下面我按第二种方案讲完整实现流程,第一种方案在 API 设计处也会给出变体说明。
3. 动手前需要准备的东西
3.1 阿里云控制台侧的准备工作清单
这套代码不是纯本地就能跑通的,需要先在阿里云控制台完成以下操作:
创建 RAM 用户。登录阿里云 RAM 控制台,新建一个用户,访问方式选择"OpenAPI 调用"。创建完成后会得到这个用户的 AccessKeyId 和 AccessKeySecret,妥善保存。这个用户是用来调用 AssumeRole 接口换取临时凭证的,权限只需要 AssumeRole 这一个动作,所以它也被称作"STS 调用者"。
创建 RAM 角色。在 RAM 角色管理页面创建一个角色,类型选择"阿里云账号",信任实体选择"当前账号"。创建时记得记录角色的 ARN(阿里云资源名称),格式类似acs:ram::1234567890123456:role/oss-upload-role,后面代码里要用。
给角色附加权限策略。参考上面的 JSON 策略,把 Action 和 Resource 换成你的实际值。如果上传目录不止一个,可以加多个 Resource 条目,但务必精确到目录级别。
给 RAM 用户授权 AssumeRole。在 RAM 用户详情页添加权限,选择系统策略AliyunSTSAssumeRoleAccess,或者自定义一个只包含sts:AssumeRole的策略。这样这个用户就只具备换取临时凭证的能力。
3.2 Python 环境和依赖安装
我的开发环境是 Python 3.10,配合虚拟环境管理依赖。需要安装的库就三个:
pip install flask aliyun-python-sdk-sts oss2flask:Web 框架,提供 API 接口aliyun-python-sdk-sts:阿里云 STS SDK,负责调用 AssumeRoleoss2:阿里云 OSS Python SDK,客户端用它来做实际的文件上传
版本方面,截至本文写作时,aliyun-python-sdk-sts的最新稳定版本是 3.x,oss2是 2.18.x,直接安装最新版即可。建议把依赖写进requirements.txt,方便项目迁移部署。
4. Flask API 获取 STS 临时凭证的完整实现
4.1 项目目录结构
我习惯把配置、STS 服务和 API 路由分开写,这样后期维护起来不用在一堆代码里翻找:
oss-sts-flask/ ├── app.py # Flask 应用入口 ├── config.py # 配置信息(OSS Bucket、角色 ARN 等) ├── services/ │ └── sts_service.py # STS 凭证获取逻辑 └── requirements.txt # 依赖清单4.2 配置文件的写法
# config.py import os class Config: # 阿里云账号 AccessKey(RAM 用户的,不是主账号的) ACCESS_KEY_ID = os.getenv("ALIYUN_ACCESS_KEY_ID", "your-ram-user-access-key-id") ACCESS_KEY_SECRET = os.getenv("ALIYUN_ACCESS_KEY_SECRET", "your-ram-user-access-key-secret") # RAM 角色的 ARN ROLE_ARN = os.getenv("ALIYUN_ROLE_ARN", "acs:ram::1234567890123456:role/oss-upload-role") # 角色会话名称,用于审计,建议带上业务标识 ROLE_SESSION_NAME = os.getenv("ROLE_SESSION_NAME", "flask-upload-session") # 临时凭证有效期,单位秒,默认 3600(1小时) DURATION_SECONDS = int(os.getenv("STS_DURATION_SECONDS", "3600")) # OSS 相关配置 OSS_BUCKET = "your-bucket-name" OSS_REGION = "oss-cn-hangzhou" OSS_ENDPOINT = f"https://{OSS_REGION}.aliyuncs.com"这里有几个细节要说明:
- 不要把 AccessKey 硬编码写在代码里,通过环境变量读取是基本素养。配置文件里我写了默认值,实际部署时一定要用环境变量覆盖。
OSS_REGION要和你创建 Bucket 时选择的地域保持一致,后缀是固定的。你在华东1(杭州)建的 Bucket,就是oss-cn-hangzhou;华北2(北京)就是oss-cn-beijing。写错了会报连接错误或者 Bucket 不存在。ROLE_SESSION_NAME建议带上业务标识,比如订单号、用户 ID,后期排查问题时能追溯到具体是哪次会话申请的凭证。
4.3 STS 服务封装
# services/sts_service.py import json from aliyunsdkcore.client import AcsClient from aliyunsdkcore.request import CommonRequest from aliyunsdkcore.acs_exception.exceptions import ClientException, ServerException def get_sts_token(config): """获取 STS 临时凭证""" client = AcsClient( config.ACCESS_KEY_ID, config.ACCESS_KEY_SECRET, "cn-hangzhou" # STS 服务的 region,固定填 cn-hangzhou 即可 ) request = CommonRequest() request.set_method("POST") request.set_domain("sts.aliyuncs.com") request.set_version("2015-04-01") request.set_action_name("AssumeRole") request.set_protocol_type("https") request.add_query_param("RoleArn", config.ROLE_ARN) request.add_query_param("RoleSessionName", config.ROLE_SESSION_NAME) request.add_query_param("DurationSeconds", config.DURATION_SECONDS) try: response = client.do_action_with_exception(request) data = json.loads(response) credentials = data.get("Credentials", {}) return { "access_key_id": credentials.get("AccessKeyId"), "access_key_secret": credentials.get("AccessKeySecret"), "security_token": credentials.get("SecurityToken"), "expiration": credentials.get("Expiration") } except (ClientException, ServerException) as e: # 记录日志并抛出业务异常,由上层 API 统一处理 raise RuntimeError(f"Failed to get STS token: {str(e)}")这里我用了 CommonRequest 而不是专门的 AssumeRoleRequest。aliyun-python-sdk-sts这个包虽然提供了专门的数据类,但 CommonRequest 的写法更灵活,而且依赖更少,代码也更直观。如果你喜欢用了类型提示的写法,也可以用 SDK 自带的AssumeRoleRequest,逻辑是一样的。
region为什么固定填cn-hangzhou?因为 STS 服务的接入点是全局统一的,不区分地域,你从哪个 region 的服务器请求都行,但请求域名固定是sts.aliyuncs.com。
4.4 Flask API 接口层面的设计
# app.py from flask import Flask, jsonify, request from config import Config from services.sts_service import get_sts_token app = Flask(__name__) @app.route("/api/oss/token", methods=["GET"]) def get_upload_token(): """获取 OSS 上传所需的 STS 临时凭证""" config = Config() # 可选的业务参数:指定上传目录前缀 upload_dir = request.args.get("dir", "uploads") try: token_info = get_sts_token(config) # 拼接客户端上传所需的额外信息 result = { "access_key_id": token_info["access_key_id"], "access_key_secret": token_info["access_key_secret"], "security_token": token_info["security_token"], "expiration": token_info["expiration"], "endpoint": config.OSS_ENDPOINT, "bucket": config.OSS_BUCKET, "upload_dir": upload_dir, "status": "success" } return jsonify(result), 200 except Exception as e: app.logger.error(f"Get STS token failed: {e}") return jsonify({"status": "error", "message": "获取临时凭证失败"}), 500 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)接口设计上有两个细节值得展开:
- 我用了 GET 方法,因为获取凭证本身是幂等操作,不改变服务端状态。但如果你有"每次上传都一定要申请新凭证"的需求,改成 POST 也行。实际项目中我用得更多的是 POST,因为通常还要带上一些业务参数做鉴权。
upload_dir参数允许客户端指定上传目录前缀,如果业务上有多个目录分别存储不同类型的文件,这个参数就能派上用场。但注意,如果你要严格限制目录,需要在 STS 的权限策略里把路径固定死,不能客户端传什么就允许什么。
4.5 客户端使用临时凭证上传 OSS
拿到 Flask API 返回的数据后,客户端(可以是普通 Python 脚本、浏览器或者移动端)用 oss2 完成上传。核心代码如下:
import oss2 # 假设 data 是上面 API 返回的 JSON 数据 auth = oss2.StsAuth( data["access_key_id"], data["access_key_secret"], data["security_token"] ) bucket = oss2.Bucket( auth, data["endpoint"], data["bucket"] ) # 构造上传对象名,注意使用 API 返回的 upload_dir 作为前缀 object_name = f"{data['upload_dir']}/{uuid.uuid4().hex}.jpg" # 从本地文件上传 bucket.put_object_from_file(object_name, "/path/to/local/file.jpg")这段代码的关键点是oss2.StsAuth,它和我们平时用的oss2.Auth不一样——多传了一个security_token。如果用Auth初始化,OSS 服务器会因为你没有携带临时令牌而拒绝访问。
注意到 object 名称前面的upload_dir一定要和 STS 权限策略里的资源路径前缀对应上。比如权限策略限定的是uploads/*,那这里upload_dir就必须是uploads,否则即使凭证有效,OSS 也会抛出AccessDenied异常。这个坑至少有一半的初次使用者会踩。
如果你做的是"服务端中转上传"模式,可以省略上面这段客户端代码,直接在 Flask 服务端用同样的方式初始化 bucket 对象,然后调用put_object_from_file接收客户端传过来的文件。注意此时upload_dir的校验逻辑由你服务端自己保证,不能依赖客户端传参。
5. 实际上线过程中最容易踩的四个问题
5.1 临时凭证过期导致的上传失败
STS 临时凭证默认有效期 3600 秒,如果你客户端上传的是超大文件(比如几个 GB 的视频),上传时间可能超过凭证有效期,就会在中途报AccessDenied或RequestTimeTooSkewed错误。
解决方案有两种:
- 如果上传不会超过 30 分钟,把
DurationSeconds调到 1800 或 3600 就够用了,不用过度追求长时间 - 如果上传时间可能超过 1 小时,改用 OSS 的断点续传功能(
bucket.resumable_upload),并且在上传过程中监控凭证的过期时间,在过期前重新申请新凭证并重试
5.2 策略权限配置正确但仍然报 AccessDenied
这是最迷惑人的问题之一。你确认了权限策略的 Action 包含oss:PutObject,Resource 路径也填对了,但上传时还是被拒绝。我遇到过的真实原因有两种:
- OSS 的 Policy 判断是基于最终的 object 名称来匹配的,如果你在
put_object_from_file时指定的 key 是upload/2024/01/file.jpg,但策略里写的是uploads/*,那就会因为前缀不匹配而被拒绝。检查一下你是不是把upload和uploads混用了。 - RAM 角色的信任策略没配置好。如果代码报的是没有权限调用 AssumeRole,可能是 RAM 用户本身缺少
sts:AssumeRole授权,或者是信任策略中的 Principal 没有包含这个用户。
5.3 客户端直传时 CORS 配置缺失
如果你是在浏览器里直接用 JS SDK 上传,OSS Bucket 必须配置 CORS 规则。否则浏览器会拦截 OSS 返回的跨域响应,表现为上传请求发出去了,但浏览器控制台报跨域错误,上传结果一片空白。
配置 CORS 的位置在 OSS Bucket 的"数据安全 → 跨域设置"里,需要添加一条规则,允许的来源写你的前端域名(不加路径),允许的 Method 勾选 PUT、POST、GET,允许的 Header 填*,暴露的 Header 填ETag。
5.4 Flask 服务性能瓶颈
STS 凭证获取本身是一个同步的 HTTP 请求到阿里云 STS 服务,网络往返大约几十到一百毫秒。如果你的 Flask API 需要支撑高并发调用,建议在 Redis 里加一层短期缓存——凭证还有效期内直接从缓存返回,避免每次都打到 STS 接口。但缓存的 key 一定要包含upload_dir等业务参数,不要所有接口共用一个缓存。
import time # 伪代码示例 cached_token = redis_client.get(f"sts_token:{upload_dir}") if cached_token: return json.loads(cached_token) # 没有缓存,走真实 STS 调用 token_info = get_sts_token(config) redis_client.setex(f"sts_token:{upload_dir}", token_info["expiration"], json.dumps(token_info))注意缓存的过期时间建议比凭证的实际过期时间提前 60 秒,防止凭证刚好在客户端上传前一刻失效。
6. 安全加固和上线前检查清单
这套方案本身的安全性建立在几个基础上:RAM 用户权限最小化、角色权限最小化、临时凭证生命周期管理、和网络传输安全。结合我实际项目的经验,下面这个清单值得逐项确认:
- [ ] RAM 用户的 AccessKey 是否启用了轮换机制?至少每 90 天更换一次,不要把密钥写死在代码仓库里
- [ ] RAM 角色的权限策略是否限定到了具体 Bucket 和目录?是否存在"*"通配符滥用?
- [ ] Flask API 是否做了认证?别人拿到你的接口地址就能直接要临时凭证的话,等于免费送人上传通道,必须在接口层加上你自己的 Token 鉴权或签名机制
- [ ] 是否启用了 HTTPS?Flask 如果直接暴露公网,至少要用 Nginx 做一层 HTTPS 转发,不要裸奔 HTTP
- [ ] 是否配置了 STS 操作审计?阿里云控制台的操作审计功能可以记录所有 STS AssumeRole 调用记录,出了问题能追溯到客户端 IP
- [ ] 上传的文件类型是否做了校验?OSS 本身不关心文件内容是什么,你要在客户端或服务端做扩展名、MIME 类型、文件大小的校验
另外一个容易被忽略的点是:OSS Bucket 的读写权限建议设为"私有"(Private),即使你用的是临时凭证,也不建议把 Bucket 开成公共读。客户端上传成功后,如果需要展示文件,通过 STS 临时凭证生成签名 URL 给前端访问,而不是直接暴露一个公共可读的地址。
7. 临时凭证的获取流程能否做进一步优化
在基本流程跑通之后,我建议你在项目里做两个小的改进,会让整个系统更健壮:
第一个是凭证缓存。前面提到过 Redis 缓存,这里说一下具体实现时的细节。缓存 key 建议带上业务标识,比如sts:upload:uid:{user_id}:{dir},这样多个业务隔离缓存,互不干扰。缓存的 value 直接存 STS 返回的整个 Credentials 字典,读取时反序列化即可。要特别注意缓存的过期时间设置——凭证的Expiration字段是一个具体的时间点,缓存有效期应该设置为expiration - now - 60s,而不是简单地设置一个固定值,这样才能保证不会缓存到已经过期的凭证。
第二个是错误码的规范返回。目前我的 API 在获取凭证失败时统一返回 500,但实际分不清是参数问题、权限问题还是 STS 服务异常。建议给异常分分类:
# 伪代码示例 if "InvalidParameter" in str(e): return jsonify({"status": "error", "code": "INVALID_PARAM", "message": "参数错误"}), 400 elif "Forbidden" in str(e): return jsonify({"status": "error", "code": "FORBIDDEN", "message": "无权操作"}), 403 else: return jsonify({"status": "error", "code": "STS_ERROR", "message": "STS服务异常"}), 500这样客户端拿到不同的 code 时,可以给用户呈现不同的提示信息,排查问题也更高效。
8. 一套可复用的 Flask 服务端参考代码
前面讲了很多原理和细节,这里把 Flask 服务端的完整代码整理成一个更贴近生产环境的版本,包含鉴权、日志和异常处理。你可以直接复制改造成自己的项目。
# app.py - 生产环境参考版 import logging import uuid from functools import wraps import oss2 from flask import Flask, jsonify, request from aliyunsdkcore.client import AcsClient from aliyunsdkcore.request import CommonRequest app = Flask(__name__) logging.basicConfig(level=logging.INFO) # 配置化,实际放在 config.py 或环境变量中 ACCESS_KEY_ID = "your-ram-access-key-id" ACCESS_KEY_SECRET = "your-ram-access-key-secret" ROLE_ARN = "acs:ram::1234567890123456:role/oss-upload-role" ROLE_SESSION_NAME = "flask-production-session" DURATION_SECONDS = 3600 OSS_BUCKET = "your-bucket-name" OSS_ENDPOINT = "https://oss-cn-hangzhou.aliyuncs.com" # 简单接口鉴权,生产环境建议替换为 JWT 或独立认证服务 API_TOKEN = "your-secret-api-token" def require_token(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get("X-API-Token") if token != API_TOKEN: return jsonify({"status": "error", "message": "Unauthorized"}), 401 return f(*args, **kwargs) return decorated def get_sts_token(): """调用 STS AssumeRole 获取临时凭证""" client = AcsClient(ACCESS_KEY_ID, ACCESS_KEY_SECRET, "cn-hangzhou") request = CommonRequest() request.set_method("POST") request.set_domain("sts.aliyuncs.com") request.set_version("2015-04-01") request.set_action_name("AssumeRole") request.set_protocol_type("https") request.add_query_param("RoleArn", ROLE_ARN) request.add_query_param("RoleSessionName", ROLE_SESSION_NAME) request.add_query_param("DurationSeconds", DURATION_SECONDS) response = client.do_action_with_exception(request) import json data = json.loads(response) return data["Credentials"] @app.route("/api/oss/token", methods=["GET"]) @require_token def issue_upload_token(): """颁发临时上传凭证""" upload_dir = request.args.get("dir", "uploads") if not upload_dir or not upload_dir.startswith("uploads"): return jsonify({"status": "error", "message": "invalid dir"}), 400 try: creds = get_sts_token() return jsonify({ "status": "success", "access_key_id": creds["AccessKeyId"], "access_key_secret": creds["AccessKeySecret"], "security_token": creds["SecurityToken"], "expiration": creds["Expiration"], "endpoint": OSS_ENDPOINT, "bucket": OSS_BUCKET, "upload_dir": upload_dir }) except Exception as e: app.logger.error(f"STS token error: {e}") return jsonify({"status": "error", "message": "internal error"}), 500 @app.route("/api/oss/upload", methods=["POST"]) @require_token def server_side_upload(): """服务端中转上传(备选方案)""" upload_dir = request.form.get("dir", "uploads") file = request.files.get("file") if not file: return jsonify({"status": "error", "message": "no file"}), 400 creds = get_sts_token() auth = oss2.StsAuth(creds["AccessKeyId"], creds["AccessKeySecret"], creds["SecurityToken"]) bucket = oss2.Bucket(auth, OSS_ENDPOINT, OSS_BUCKET) object_name = f"{upload_dir}/{uuid.uuid4().hex}_{file.filename}" try: bucket.put_object(object_name, file.stream) return jsonify({"status": "success", "url": f"https://{OSS_BUCKET}.{OSS_ENDPOINT.replace('https://', '')}/{object_name}"}) except oss2.exceptions.OssError as e: app.logger.error(f"OSS upload error: {e}") return jsonify({"status": "error", "message": "upload failed"}), 500 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)这里有两种接口,/api/oss/token是客户端直传模式,/api/oss/upload是服务端中转模式。你根据实际需求选择保留其中一种即可。生产环境部署时记得去掉debug=True,用 gunicorn 或者 uWSGI 运行:
gunicorn -w 4 -b 0.0.0.0:5000 app:app服务端中转方案在拿到文件流之后,还可以顺手做文件类型校验、大小限制,甚至调用病毒扫描服务,安全性上更有保障。直传模式胜在简单高效,优缺点前面已经对比过。
9. 常见问题速查表
这个表格是我整理的实际开发中最高频的问题,覆盖了从配置到运行的各个环节。如果你调试时遇到类似报错,可以直接按表格排查:
| 报错信息 | 根本原因 | 解决方案 |
|---|---|---|
InvalidAccessKeyId.NotFound | AccessKeyId 配置错误或已禁用 | 检查 RAM 用户密钥是否正确,是否被禁用 |
Forbidden.RAM | RAM 用户没有 AssumeRole 权限 | 给 RAM 用户添加AliyunSTSAssumeRoleAccess策略 |
AssumeRole access denied | 角色信任策略未包含该用户 | 编辑角色信任策略,在 Principal 中加入该 RAM 用户 |
AccessDenied(上传时) | 临时凭证的权限策略和 object 路径不匹配 | 检查上传的 object 前缀是否在权限策略 Resource 中 |
RequestTimeTooSkewed | 客户端系统时间和 OSS 服务器时间偏差过大 | 校准客户端系统时间,尤其注意 Docker 容器内的时区问题 |
InvalidSecurityToken.Expired | 临时凭证已过期 | 重新获取凭证,检查缓存过期时间设置 |
SignatureDoesNotMatch | SecurityToken 没有传给 OSS SDK | 确保使用oss2.StsAuth而非oss2.Auth |
| 浏览器跨域报错 | OSS Bucket 未配置 CORS | 在 OSS 控制台配置允许的来源和 Method |
排查这类问题有个通法:先逐步排除配置问题,然后看权限,最后看时间。配置错了报错信息一般很直接,权限问题要看策略和实际请求的资源是否匹配,时间问题则比较隐蔽——我遇到过本地开发电脑和服务器时区不一致导致的诡异报错,花了半天才定位到是时间偏移导致的签名校验失败。
我个人在实际操作中的体会是,STS 这套机制设计得其实并不复杂,难的是权限模型的理解。只要你把 RAM 用户、角色、权限策略这三者之间的关系理清楚,后面的开发就是按部就班的机械工作。建议你先用一个测试 Bucket 把完整链路跑通,再逐步收紧权限,不要一上来就追求最严格的策略,否则容易在权限配置上卡住很久。最后再说一个小技巧:阿里云控制台 OSS 提供了一个验证工具,可以模拟上传请求,调试哪一步出错会直接给出对应的 Policy 校验结果,排查权限问题比看日志快得多。