MinIO STS API AssumeRoleWithClientGrants 详解:基于 OAuth2 客户端凭据签发临时 S3 凭据
本文围绕 MinIO 的 STS 扩展接口 AssumeRoleWithClientGrants 展开:它允许已经完成 OAuth 2.0 客户端凭据授权(client credential grants)的应用程序,仅凭身份提供商(Keycloak、Okta 等)签发的 JWT 访问令牌,向 MinIO 换取一组临时安全凭据(Access Key、Secret Key、Session Token),全程无需持有 MinIO 根凭据。读完本文,你将掌握该 API 的完整请求参数与限制、底层校验与签发链路(源码级)、DurationSeconds 的取值规则、会话策略(Session Policy)机制,以及 Go / Python 两种可直接运行的客户端实战示例。
一、核心概念:为什么需要 ClientGrants 换发临时凭据
MinIO 的 STS(Security Token Service)层在 cmd/sts-handlers.go 中实现了多个"角色假设"动作,其中 AssumeRoleWithClientGrants 是 MinIO 自有的 STS 扩展 API,专门服务 OAuth 2.0 客户端凭据模式:
应用 ──(client_id + client_secret)──> IDP 令牌端点 ──> JWT access token
应用 ──(Token=<jwt>)──> MinIO /?Action=AssumeRoleWithClientGrants
MinIO ──> { AccessKeyId, SecretAccessKey, SessionToken, Expiration }
从源码常量定义可以看出,clientGrants = "AssumeRoleWithClientGrants" 与 AssumeRoleWithWebIdentity、AssumeRoleWithLDAPIdentity、AssumeRoleWithCertificate、AssumeRoleWithCustomToken 并列,都属于 STS 动作(见 cmd/sts-handlers.go 第 61-66 行的动作常量)。
它的价值在于凭据隔离:
- 调用
AssumeRoleWithClientGrants不需要使用 MinIO 默认(根)凭据。客户端应用可以分发出去,请求临时凭据时只需携带从身份提供商获得的 JWT,调用者身份由该 JWT 验证; - 返回的临时凭据由 Access Key、Secret Key 和 Security Token 三部分组成,应用使用它们对 MinIO API 调用做签名,凭据到期自动失效,无需吊销静态密钥;
- 默认临时凭据有效期为 1 小时,可通过可选参数
DurationSeconds指定 900 秒(15 分钟)到 365 天 之间的时长。
二、API 请求参数详解
AssumeRoleWithClientGrants 的 HTTP 请求以 POST 方法发出,参数通过 URL 查询串(或表单)传递。四个参数如下。
Token(必填)
身份提供商签发的 OAuth 2.0 访问令牌。应用必须先使用 client credential grants 完成应用身份认证、拿到该令牌,才能发起 AssumeRoleWithClientGrants 调用。
| 属性 | 值 |
|---|---|
| 类型 | String |
| 长度约束 | 最小 4 个字符,最大 2048 个字符 |
| 是否必填 | 是 |
Version(必填)
STS API 版本标识,唯一支持的值为 2011-06-15。该值从 AWS STS API 规范借用而来,出于兼容性考虑(源码中对应常量 stsAPIVersion = "2011-06-15")。
| 属性 | 值 |
|---|---|
| 类型 | String |
| 是否必填 | 是 |
DurationSeconds(可选)
临时凭据的有效期(秒)。取值范围为 900(15 分钟)至 31536000(365 天),超出范围请求将失败;未指定时默认 3600 秒(1 小时)。
| 属性 | 值 |
|---|---|
| 类型 | Integer |
| 有效范围 | 最小 900,最大 31536000 |
| 是否必填 | 否 |
源码级说明:该参数的解析在 internal/config/identity/openid/openid.go 的 GetDefaultExpiration 函数中完成(约第 601-630 行),其逻辑值得注意:
- 若设置了环境变量
MINIO_STS_DURATION(源码引用config.EnvMinioStsDuration),该值会作为全局默认时长,优先级高于请求里的DurationSeconds; - 否则解析
DurationSeconds,小于MinExpiration(900)或大于MaxExpiration(31536000)时返回ErrInvalidDuration,最终映射为 STS 错误InvalidParameterValue; - 两者都未提供时返回 1 小时。
这意味着管理员可以通过 MINIO_STS_DURATION 统一约束整个集群 STS 凭据的存活时长,而 DurationSeconds 是请求级的覆盖手段(在无全局设置时生效)。
Policy(可选)
JSON 格式的 IAM 策略,作为内联会话策略使用。它是可选的:传入后 MinIO 会签发新的临时凭据,会话最终权限 = 客户端凭据绑定的策略 ∩ 该 Policy 指定的权限(交集收窄,不可放大)——即不能用它获得超出所假设策略允许的权限。
| 属性 | 值 |
|---|---|
| 类型 | String |
| 有效范围 | 最小长度 1,最大长度 2048 |
| 是否必填 | 否 |
源码级说明:会话策略处理在 cmd/sts-handlers.go 的 populateSessionPolicy 方法中(约第 98-133 行):策略先经 policy.ParseConfig 解析校验(Version 字段必须非空,否则报错),随后序列化并与常量 maxSTSSessionPolicySize = 2048 比较,超过即返回 errSessionPolicyTooLarge;校验通过后以 Base64 形式写入 JWT 声明 policy.SessionPolicyName,随临时凭据一起签发。
三、响应结构与错误码
响应元素
AssumeRoleWithClientGrants 的 XML 响应与 AWS STS AssumeRoleWithWebIdentity 的响应元素风格保持一致。响应类型定义见 cmd/sts-datatypes.go 的 AssumeRoleWithClientGrantsResponse 与 ClientGrantsResult(约第 129-177 行):包含 AssumedRoleUser(临时凭据标识,含 Arn 与 AssumeRoleId)、Credentials(AccessKeyId / SecretAccessKey / SessionToken / Expiration)、Audience(即请求客户端的 client ID)、PackedPolicySize、Provider(令牌 iss 声明)以及 SubjectFromToken(令牌 sub 声明)。
示例 POST 请求
http://minio.cluster:9000?Action=AssumeRoleWithClientGrants&DurationSeconds=3600&Token=eyJ4NXQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJraWQiOiJOVEF4Wm1NeE5ETXlaRGczTVRVMVpHTTBNekV6T0RKaFpXSTRORE5sWkRVMU9HRmtOakZpTVEiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiYXpwIjoiUG9FZ1hQNnVWTzQ1SXNFTlJuZ0RYajVBdTVZYSIsImlzcyI6Imh0dHBzOlwvXC9sb2NhbGhvc3Q6OTQ0M1wvb2F1dGgyXC90b2tlbiIsImV4cCI6MTU0MTgwOTU4MiwiaWF0IjoxNTQxODA1OTgyLCJqdGkiOiI2Y2YyMGIwZS1lNGZmLTQzZmQtYTdiYS1kYTc3YTE3YzM2MzYifQ.Jm29jPliRvrK6Os34nSK3rhzIYLFjE__zdVGNng3uGKXGKzP3We_i6NPnhA0szJXMOKglXzUF1UgSz8MctbaxFS8XDusQPVe4LkB_45hwBm6TmBxzui911nt-1RbBLN_jZIlvl2lPrbTUH5hSn9kEkph6seWanTNQpz9tNEoVa6R_OX3kpJqxe8tLQUWw453A1JTwFNhdHa6-f1K8_Q_eEZ_4gOYINQ9t_fhTibdbkXZkJQFLop-Jwoybi9s4nwQU_dATocgcufq5eCeNItQeleT-23lGxIz0X7CiJrJynYLdd-ER0F77SumqEb5iCxhxuf4H7dovwd1kAmyKzLxpw&Version=2011-06-15
示例响应
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleWithClientGrantsResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleWithClientGrantsResult>
<AssumedRoleUser>
<Arn/>
<AssumeRoleId/>
</AssumedRoleUser>
<Credentials>
<AccessKeyId>Y4RJU1RNFGK48LGO9I2S</AccessKeyId>
<SecretAccessKey>sYLRKS1Z7hSjluf6gEbb9066hnx315wHTiACPAjg</SecretAccessKey>
<Expiration>2019-08-08T20:26:12Z</Expiration>
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZNFJKVTFSTkZHSzQ4TEdPOUkyUyIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTQxODExMDcxLCJpYXQiOjE1NDE4MDc0NzEsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiYTBiMjc2MjktZWUxYS00M2JmLTg3MzktZjMzNzRhNGNkYmMwIn0.ewHqKVFTaP-j_kgZrcOEKroNUjk10GEp8bqQjxBbYVovV0nHO985VnRESFbcT6XMDDKHZiWqN2vi_ETX_u3Q-w</SessionToken>
</Credentials>
</AssumeRoleWithClientGrantsResult>
<ResponseMetadata/>
</AssumeRoleWithClientGrantsResponse>
错误响应格式与 AWS STS AssumeRoleWithWebIdentity 的错误风格一致,由 cmd/sts-errors.go 中的 STSErrorResponse 结构输出。与 ClientGrants 直接相关的错误码有:
| Code | HTTP 状态 | 含义 |
|---|---|---|
ExpiredToken |
400 | 传入的 client grants 令牌已过期或无效,需从身份提供商重新获取(ErrSTSClientGrantsExpiredToken) |
InvalidClientGrantsToken |
400 | MinIO 无法校验该令牌(ErrSTSInvalidClientGrantsToken) |
MissingParameter |
400 | 缺少必填参数,如 Version 值不是 2011-06-15 |
InvalidParameterValue |
400 | 参数非法或越界,如 DurationSeconds 超范围、sub 声明缺失 |
AccessDenied |
403 | 当前身份不允许生成临时凭据 |
STSIAMNotInitialized |
503 | 服务端 IAM 尚未完成初始化 |
InternalError |
500 | 内部错误或上游依赖故障 |
四、服务端实现链路(源码解析)
路由注册:URL 契约的硬约束
cmd/sts-handlers.go 的 registerSTSRouter(约第 139-189 行)注册了 AssumeRoleWithClientGrants 的专属路由:
// AssumeRoleWithClientGrants
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
Queries(stsAction, clientGrants).
Queries(stsVersion, stsAPIVersion).
Queries(stsToken, "{Token:.*}")
这解释了该 API 的 URL 契约:Action=AssumeRoleWithClientGrants、Version=2011-06-15 和 Token 三个查询参数是路由匹配的硬性前提。另外还存在一条宽松路由:Content-Type: application/x-www-form-urlencoded 且无查询串的 POST 请求会进入 AssumeRoleWithSSO 统一入口,内部再按表单中的 Action 分发到 clientGrants / webIdentity 分支——这正是 minio-go 等 SDK 走表单编码路径的原因。
核心处理流程
AssumeRoleWithClientGrants 直接委托给 AssumeRoleWithSSO(约第 638-646 行),后者完成了全部校验与签发逻辑,关键步骤如下:
- 表单解析与版本校验:
parseForm后检查Version,不匹配即返回MissingParameter; - RoleArn 处理:若请求携带
RoleArn,则通过globalIAMSys.GetRolePolicy解析对应策略;未携带时回退到 claim 驱动模式(使用openid.DummyRoleARN)。对配置了 claim 策略的 IDP,未识别的 RoleArn 会被容忍并回退,目的是兼容 AWS SDK/CLI 这类"必填 RoleArn"的客户端; - JWT 校验:
globalIAMSys.OpenIDConfig.Validate完成签名验证、aud与 client ID 匹配、过期检查与DurationSeconds合法性检查;令牌过期时按 action 分别返回ErrSTSClientGrantsExpiredToken或ErrSTSWebIdentityExpiredToken; - 策略绑定:claim 模式下从 JWT 读取策略声明(
iamPolicyClaimNameOpenID()指定的 claim),经globalIAMSys.CurrentPolicies合并为策略名;声明缺失或策略不存在时返回InvalidParameterValue; sub声明强制校验:JWT 必须携带sub声明,缺失即拒绝;ParentUser由sha256("openid:" + sub + ":" + iss)的 Base64URL 编码生成(约第 538-548 行)。之所以做哈希,是因为父用户会用于策略映射的文件名,必须只含合法文件名字符且长度有界;- 会话策略与签发:
populateSessionPolicy处理后,用getTokenSigningKey(站点复制启用时取复制凭据的密钥)调用auth.GetNewCredentialsWithMetadata生成凭据,最后经globalIAMSys.SetTempUser落库,并通过站点复制 hook 将临时账户同步到对端站点。
从源码结构看,AssumeRoleWithClientGrants 与 AssumeRoleWithWebIdentity 共享 AssumeRoleWithSSO 的完整校验管线,区别仅在于响应类型(ClientGrantsResult vs WebIdentityResult)与过期错误码。
五、实战:配置身份提供商并调用 API
1. 启动配置了 OpenID 客户端的 MinIO
按官方文档 docs/sts/client-grants.md 给出的方式,通过环境变量为 MinIO 启用 OpenID 身份提供商(以 Keycloak 为例):
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
export MINIO_IDENTITY_OPENID_CONFIG_URL=http://localhost:8080/auth/realms/demo/.well-known/openid-configuration
export MINIO_IDENTITY_OPENID_CLIENT_ID="843351d4-1080-11ea-aa20-271ecba3924a"
minio server /mnt/export
其中 MINIO_IDENTITY_OPENID_CONFIG_URL 指向 IDP 的 OpenID 发现端点,MINIO_IDENTITY_OPENID_CLIENT_ID 必须是 IDP 中已注册的 OAuth 客户端 ID——请求 JWT 的 aud 声明会与之比对。Keycloak 客户端与凭据的创建步骤可参考仓库内的 docs/sts/keycloak.md。
2. Go 客户端:minio-go v7 的 STS ClientGrants 凭据提供器
仓库自带完整示例 docs/sts/client-grants.go,运行方式:
$ go run client-grants.go -cid PoEgXP6uVO45IsENRngDXj5Au5Ya -csec eKsw6z8CtOJVBtrOWvhRWL4TUCga
支持四个命令行参数:-sts-ep(STS 端点,默认 http://localhost:9000)、-idp-ep(IDP 令牌端点,默认指向 Keycloak 的 token 接口)、-cid(client ID)、-csec(client secret)。核心流程分两步:
// 第一步:用 client_id/client_secret 向 IDP 换取 access token
func getTokenExpiry() (*credentials.ClientGrantsToken, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")
req, err := http.NewRequest(http.MethodPost, idpEndpoint, strings.NewReader(data.Encode()))
// ...
req.SetBasicAuth(clientID, clientSecret)
// ... 解析响应中的 access_token 与 expires_in
return &credentials.ClientGrantsToken{Token: idpToken.AccessToken, Expiry: idpToken.Expiry}, nil
}
// 第二步:把获取 token 的闭包交给 minio-go 的 STS 凭据提供器
sts, err := credentials.NewSTSClientGrants(stsEndpoint, getTokenExpiry)
opts := &minio.Options{
Creds: sts,
BucketLookup: minio.BucketLookupAuto,
}
clnt, err := minio.New(u.Host, opts)
// 直接使用临时凭据上传对象,SDK 会在凭据过期前自动刷新
d := bytes.NewReader([]byte("Hello, World"))
n, err := clnt.PutObject(context.Background(), "my-bucketname", "my-objectname", d, d.Size(), minio.PutObjectOptions{})
运行成功后输出临时凭据:
##### Credentials
{
"accessKey": "NUIBORZYTV2HG2BMRSXR",
"secretKey": "qQlP5O7CFPc5m5IXf1vYhuVTFj7BRVJqh0FqZ86S",
"expiration": "2018-08-21T17:10:29-07:00",
"sessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJOVUlCT1JaWVRWMkhHMkJNUlNYUiIsImF1ZCI6IlBvRWdYUDZ1Vk80NUlzRU5SbmdEWGo1QXU1WWEiLCJhenAiOiJQb0VnWFA2dVZPNDVJc0VOUm5nRFhqNUF1NVlhIiwiZXhwIjoxNTM0ODk2NjI5LCJpYXQiOjE1MzQ4OTMwMjksImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0Ojk0NDMvb2F1dGgyL3Rva2VuIiwianRpIjoiNjY2OTZjZTctN2U1Ny00ZjU5LWI0MWQtM2E1YTMzZGZiNjA4In0.eJONnVaSVHypiXKEARSMnSKgr-2mlC2Sr4fEGJitLcJF_at3LeNdTHv0_oHsv6ZZA3zueVGgFlVXMlREgr9LXA"
}
3. Python 客户端:botocore 自定义凭据提供器
仓库同时提供了 Python 实现,由三部分组成:
- docs/sts/client_grants/init.py:实现 botocore 的
CredentialProvider接口,load()返回RefreshableCredentials,凭证过期后自动重新走"IDP 换 token → STS 换凭据"流程;_create_credentials_fetcher中组装 STS 查询串时使用的正是本文介绍的四个参数:Action=AssumeRoleWithClientGrants、Token、DurationSeconds(取 IDP 返回的expires_in)、Version=2011-06-15; - docs/sts/client_grants/sts_element.py:解析 STS XML 响应元素的辅助类;
- docs/sts/client-grants.py:入口脚本,将提供器注入 boto3 会话后直接对 S3 执行上传(含 AES256 服务端加密)与下载。
使用方式:
from client_grants import ClientGrantsCredentialProvider
bc_session = get_session()
bc_session.get_component('credential_provider').insert_before(
'env',
ClientGrantsCredentialProvider('<client-id>', '<client-secret>'),
)
boto3_session = Session(botocore_session=bc_session)
s3 = boto3_session.resource('s3', endpoint_url='http://localhost:9000')
六、关键限制与注意事项小结
- 时长边界:
DurationSeconds必须在 900–31536000 秒之间;未指定时默认 3600 秒;若设置了MINIO_STS_DURATION,全局默认时长优先生效(见 internal/config/identity/openid/openid.goGetDefaultExpiration); - 权限只会收窄:
Policy会话策略与原策略取交集,序列化后不得超过 2048 字符(maxSTSSessionPolicySize); - JWT 要求:令牌必须通过签名与
aud校验,且必须包含sub声明;ParentUser由sub与iss联合派生,保证同一身份的策略映射稳定; - 响应格式对齐 AWS:XML 元素命名与 AWS STS 保持一致,因此 AWS SDK 家族(通过自定义 endpoint)与 minio-go 等客户端均可对接;
- 多 claim IDP 的局限:从
AssumeRoleWithSSO的注释可见,目前未提供参数来在多 claim 型 IDP 间做消歧,无RoleArn时假定请求面向默认配置的 claim 型 IDP——这一点在部署多 IDP 时需要留意。
延伸阅读
仓库中与本文相关的材料:
- 官方文档原文:docs/sts/client-grants.md
- STS 路由与处理器:cmd/sts-handlers.go
- 响应结构定义:cmd/sts-datatypes.go
- 错误码定义:cmd/sts-errors.go
- 时长校验实现:internal/config/identity/openid/openid.go
- Go 示例:docs/sts/client-grants.go;Python 示例:docs/sts/client-grants.py 与 docs/sts/client_grants
- Keycloak 配置指南:docs/sts/keycloak.md
- STS 总体说明:docs/sts/README.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00