在 Keycloak
中,如果你通过 client_id
和 client_secret
使用 client_credentials 模式获取 Token
,返回的 Token
中默认不包含租户信息(tenant info) 是正常现象。下面我来详细解释原因、影响和解决办法。
在 Keycloak
里,客户端签发的 Token
没有租户信息可能由以下原因导致:
- Scope 配置问题 :
Scope
用于声明请求访问的资源和权限,会影响Keycloak
返回的访问Token
信息。若未在授权请求里配置包含租户信息的Scope
,访问Token
可能就不会包含租户信息。例如在授权请求中添加openid
才会生成包含用户身份信息的ID Token
。 - Client-Scope 配置缺失 :
Keycloak
中的Client-Scope
能为每个客户端分配授权范围,直接影响访问Token
中的内容。若没有配置包含租户信息的Client-Scope
或自定义Mapper
,Token
可能不会包含租户信息。可通过自定义Scope/Mapper
来控制Token
中的字段 。
📌 一、问题背景
- 你使用的是:
POST /realms/{realm}/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id={your_client_id}
&client_secret={your_client_secret}
这种方式是典型的 服务间通信(Machine-to-Machine, M2M)模式,Keycloak
返回的 Token
是一个代表客户端身份的 Client Token,而不是用户 Token
。
❓ 为啥 Token 中没有租户信息
✅ 原因如下:
原因 | 说明 |
---|---|
1. Client Token 不代表用户 | client_credentials 获取的是客户端身份凭证,与用户无关,自然不会有“租户”上下文 |
2. 租户信息通常来自用户属性 | 租户字段通常是用户的一个属性(如 tenant_id ),而 client 没有绑定具体用户 |
3. 默认 Mapper 未映射租户字段 | 即使 client 有 service account ,也需手动配置 mapper 才能将租户信息写入 token |
🧩 二、Token 类型区分
Token 来源 | 用户上下文 | 是否带租户信息 | 应用场景 |
---|---|---|---|
authorization_code | ✅ 有用户 | ✅ 可带租户信息 | 用户登录 |
password (user) | ✅ 有用户 | ✅ 可带租户信息 | 用户名密码登录 |
client_credentials | ❌ 无用户 | ❌ 默认不带租户信息 | 后端服务调用 |
✅ 三、如何让 client_credentials 签发的 Token 包含租户信息?
你可以通过以下方式实现:
🔧 方法一:为 Client 开启 Service Account 并设置 Mapper
- 说明:
Mapper
(映射器)是客户端范围映射器(Client scopes -> Mappers
)
步骤:
-
在
Keycloak
管理控制台中:- 进入对应
Realm
- 找到你的
Client
(即使用client_id
的那个) - 切换到 Service Accounts Settings
- 启用 Service Account Enabled
- 进入对应
-
查看该
Client
的Service Account
用户:- 页面会显示用户名,例如:
service-account-your-client-id
- 你可以给这个用户设置角色、分配属性,包括
tenant_id
- 页面会显示用户名,例如:
-
给这个用户添加自定义属性(如
tenant_id=xxx
) -
回到
Client
设置:- 菜单选择 Client Scopes > Default Client Scope
- 添加
Mapper
,把tenant_id
属性映射进token
(可以放在ID Token
或Access Token
中)
-
再次请求
Token
,你应该能在token payload
中看到tenant_id
字段
🔧 方法二:使用 Token Exchange(Token Exchange Grant)
Enable Standard Token Exchange V2 for this client
.
如果你需要模拟某个用户的租户上下文,可以先获取用户
Token
,再通过Token Exchange
换成client
的Token
,并保留租户信息。
注意: 当前客户端
client
需要启用服务账号角色(ServiceAccountsEnabled = true
)。
常见的 ClientRepresentation
对象设置如下:
// <auto-generated/>
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
namespace Keycloak.AuthServices.Sdk.Kiota.Admin.Models;
#pragma warning disable CS1591
public class ClientRepresentation : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
---
// 创建新客户端
var client = new ClientRepresentation
{
Name = authConfig.ClientName,
Description = authConfig.Description,
ClientId = authConfig.ClientId,
Secret = authConfig.ClientSecret,
//RedirectUris = ["https://localhost:5001/*"],
//WebOrigins = ["https://localhost:5001"],
Enabled = true,
PublicClient = false,
Protocol = "openid-connect",
// 允许您向 Keycloak 验证此客户端并检索专用于此客户端的访问令牌。根据 OAuth2 规范,这可以支持此客户端的'客户端凭据授权'。
ServiceAccountsEnabled = true,
// 这启用了基于标准 OpenID Connect 重定向的身份验证和授权代码。根据 OpenID Connect 或 OAuth2 规范,这启用了对此客户端的'授权代码流'的支持。
StandardFlowEnabled = true,
};
关于 ClientRepresentation
更多信息,请查看:
请求示例:
POST /realms/{realm}/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token={user-access-token}
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&client_id={your_client_id}
&client_secret={your_client_secret}
这样获得的 Token
可以继承用户中的租户信息(需要配置好 Scope
和 Mapper
)。
🔧 方法三:自定义协议 Mapper(推荐)
你可以编写或配置一个 Protocol Mapper
,让 client
的 Token
主动带上租户信息:
步骤:
- 进入
Client
的 Mappers 页面 - 创建一个新 Mapper:
- Name:
tenant_id
- Mapper Type:
User Attribute
- User Attribute:
tenant_id
- Token Claim Name:
tenant_id
- Claim Value Type:
String
- Name:
- 保存后,再次请求
Token
,应该能看到tenant_id
字段
📦 四、Token 示例对比
- 默认
Token
(client_credentials
)
{
"jti": "abc123",
"exp": 1718000000,
"nbf": 0,
"iss": "https://keycloak.example.com/auth/realms/myrealm",
"aud": "your-client-id",
"sub": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"typ": "Bearer",
"azp": "your-client-id"
}
- 带租户信息的
Token
(配置好Mapper
后)
{
"jti": "abc123",
"exp": 1718000000,
"nbf": 0,
"iss": "https://keycloak.example.com/auth/realms/myrealm",
"aud": "your-client-id",
"sub": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"typ": "Bearer",
"azp": "your-client-id",
"tenant_id": "tenant_12345"
}
此时 client
的 token
就会多一个参数 tenant_id
,同理其他参数也可以自定义映射(Mapper
)。
✅ 五、总结
问题 | 原因 | 解决方案 |
---|---|---|
client_credentials 获取的 Token 没有 tenant 信息 | client token 默认无用户上下文,不自动带租户字段 | 配置 Service Account + Mapper |
如何让 Token 包含租户信息? | 租户字段通常属于用户属性 | 给 Service Account 用户加属性并映射 |
是否支持 client 级别的租户字段? | 支持,但需自定义 Mapper | 创建 User Attribute Mapper |