首页
/ MinIO STS API AssumeRoleWithClientGrants 详解:基于 OAuth2 客户端凭据签发临时 S3 凭据

MinIO STS API AssumeRoleWithClientGrants 详解:基于 OAuth2 客户端凭据签发临时 S3 凭据

2026-09-06 20:57:03作者:幸俭卉

本文围绕 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"AssumeRoleWithWebIdentityAssumeRoleWithLDAPIdentityAssumeRoleWithCertificateAssumeRoleWithCustomToken 并列,都属于 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.goGetDefaultExpiration 函数中完成(约第 601-630 行),其逻辑值得注意:

  1. 若设置了环境变量 MINIO_STS_DURATION(源码引用 config.EnvMinioStsDuration),该值会作为全局默认时长,优先级高于请求里的 DurationSeconds
  2. 否则解析 DurationSeconds,小于 MinExpiration(900)或大于 MaxExpiration(31536000)时返回 ErrInvalidDuration,最终映射为 STS 错误 InvalidParameterValue
  3. 两者都未提供时返回 1 小时。

这意味着管理员可以通过 MINIO_STS_DURATION 统一约束整个集群 STS 凭据的存活时长,而 DurationSeconds 是请求级的覆盖手段(在无全局设置时生效)。

Policy(可选)

JSON 格式的 IAM 策略,作为内联会话策略使用。它是可选的:传入后 MinIO 会签发新的临时凭据,会话最终权限 = 客户端凭据绑定的策略 ∩ 该 Policy 指定的权限(交集收窄,不可放大)——即不能用它获得超出所假设策略允许的权限。

属性
类型 String
有效范围 最小长度 1,最大长度 2048
是否必填

源码级说明:会话策略处理在 cmd/sts-handlers.gopopulateSessionPolicy 方法中(约第 98-133 行):策略先经 policy.ParseConfig 解析校验(Version 字段必须非空,否则报错),随后序列化并与常量 maxSTSSessionPolicySize = 2048 比较,超过即返回 errSessionPolicyTooLarge;校验通过后以 Base64 形式写入 JWT 声明 policy.SessionPolicyName,随临时凭据一起签发。

三、响应结构与错误码

响应元素

AssumeRoleWithClientGrants 的 XML 响应与 AWS STS AssumeRoleWithWebIdentity 的响应元素风格保持一致。响应类型定义见 cmd/sts-datatypes.goAssumeRoleWithClientGrantsResponseClientGrantsResult(约第 129-177 行):包含 AssumedRoleUser(临时凭据标识,含 ArnAssumeRoleId)、CredentialsAccessKeyId / SecretAccessKey / SessionToken / Expiration)、Audience(即请求客户端的 client ID)、PackedPolicySizeProvider(令牌 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.goregisterSTSRouter(约第 139-189 行)注册了 AssumeRoleWithClientGrants 的专属路由:

// AssumeRoleWithClientGrants
stsRouter.Methods(http.MethodPost).HandlerFunc(httpTraceAll(sts.AssumeRoleWithClientGrants)).
    Queries(stsAction, clientGrants).
    Queries(stsVersion, stsAPIVersion).
    Queries(stsToken, "{Token:.*}")

这解释了该 API 的 URL 契约:Action=AssumeRoleWithClientGrantsVersion=2011-06-15Token 三个查询参数是路由匹配的硬性前提。另外还存在一条宽松路由:Content-Type: application/x-www-form-urlencoded 且无查询串的 POST 请求会进入 AssumeRoleWithSSO 统一入口,内部再按表单中的 Action 分发到 clientGrants / webIdentity 分支——这正是 minio-go 等 SDK 走表单编码路径的原因。

核心处理流程

AssumeRoleWithClientGrants 直接委托给 AssumeRoleWithSSO(约第 638-646 行),后者完成了全部校验与签发逻辑,关键步骤如下:

  1. 表单解析与版本校验parseForm 后检查 Version,不匹配即返回 MissingParameter
  2. RoleArn 处理:若请求携带 RoleArn,则通过 globalIAMSys.GetRolePolicy 解析对应策略;未携带时回退到 claim 驱动模式(使用 openid.DummyRoleARN)。对配置了 claim 策略的 IDP,未识别的 RoleArn 会被容忍并回退,目的是兼容 AWS SDK/CLI 这类"必填 RoleArn"的客户端;
  3. JWT 校验globalIAMSys.OpenIDConfig.Validate 完成签名验证、aud 与 client ID 匹配、过期检查与 DurationSeconds 合法性检查;令牌过期时按 action 分别返回 ErrSTSClientGrantsExpiredTokenErrSTSWebIdentityExpiredToken
  4. 策略绑定:claim 模式下从 JWT 读取策略声明(iamPolicyClaimNameOpenID() 指定的 claim),经 globalIAMSys.CurrentPolicies 合并为策略名;声明缺失或策略不存在时返回 InvalidParameterValue
  5. sub 声明强制校验:JWT 必须携带 sub 声明,缺失即拒绝;ParentUsersha256("openid:" + sub + ":" + iss) 的 Base64URL 编码生成(约第 538-548 行)。之所以做哈希,是因为父用户会用于策略映射的文件名,必须只含合法文件名字符且长度有界;
  6. 会话策略与签发populateSessionPolicy 处理后,用 getTokenSigningKey(站点复制启用时取复制凭据的密钥)调用 auth.GetNewCredentialsWithMetadata 生成凭据,最后经 globalIAMSys.SetTempUser 落库,并通过站点复制 hook 将临时账户同步到对端站点。

从源码结构看,AssumeRoleWithClientGrantsAssumeRoleWithWebIdentity 共享 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=AssumeRoleWithClientGrantsTokenDurationSeconds(取 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')

六、关键限制与注意事项小结

  1. 时长边界DurationSeconds 必须在 900–31536000 秒之间;未指定时默认 3600 秒;若设置了 MINIO_STS_DURATION,全局默认时长优先生效(见 internal/config/identity/openid/openid.go GetDefaultExpiration);
  2. 权限只会收窄Policy 会话策略与原策略取交集,序列化后不得超过 2048 字符(maxSTSSessionPolicySize);
  3. JWT 要求:令牌必须通过签名与 aud 校验,且必须包含 sub 声明;ParentUsersubiss 联合派生,保证同一身份的策略映射稳定;
  4. 响应格式对齐 AWS:XML 元素命名与 AWS STS 保持一致,因此 AWS SDK 家族(通过自定义 endpoint)与 minio-go 等客户端均可对接;
  5. 多 claim IDP 的局限:从 AssumeRoleWithSSO 的注释可见,目前未提供参数来在多 claim 型 IDP 间做消歧,无 RoleArn 时假定请求面向默认配置的 claim 型 IDP——这一点在部署多 IDP 时需要留意。

延伸阅读

仓库中与本文相关的材料:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391