惊了!Keycloak 客户端签发的 Token 竟无租户信息?

Keycloak 中,如果你通过 client_idclient_secret 使用 client_credentials 模式获取 Token,返回的 Token 中默认不包含租户信息(tenant info) 是正常现象。下面我来详细解释原因、影响和解决办法。


Keycloak 里,客户端签发的 Token 没有租户信息可能由以下原因导致:

  1. Scope 配置问题Scope 用于声明请求访问的资源和权限,会影响 Keycloak 返回的访问 Token 信息。若未在授权请求里配置包含租户信息的 Scope,访问 Token 可能就不会包含租户信息。例如在授权请求中添加 openid 才会生成包含用户身份信息的 ID Token
  2. Client-Scope 配置缺失Keycloak 中的 Client-Scope 能为每个客户端分配授权范围,直接影响访问 Token 中的内容。若没有配置包含租户信息的 Client-Scope 或自定义 MapperToken 可能不会包含租户信息。可通过自定义 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 未映射租户字段即使 clientservice account,也需手动配置 mapper 才能将租户信息写入 token

🧩 二、Token 类型区分

Token 来源用户上下文是否带租户信息应用场景
authorization_code✅ 有用户✅ 可带租户信息用户登录
password (user)✅ 有用户✅ 可带租户信息用户名密码登录
client_credentials❌ 无用户❌ 默认不带租户信息后端服务调用

✅ 三、如何让 client_credentials 签发的 Token 包含租户信息?

你可以通过以下方式实现:


🔧 方法一:为 Client 开启 Service Account 并设置 Mapper

  • 说明:Mapper (映射器)是客户端范围映射器(Client scopes -> Mappers

在这里插入图片描述

步骤:
  1. Keycloak 管理控制台中:

    • 进入对应 Realm
    • 找到你的 Client(即使用 client_id 的那个)
    • 切换到 Service Accounts Settings
    • 启用 Service Account Enabled
  2. 查看该 ClientService Account 用户:

    • 页面会显示用户名,例如:service-account-your-client-id
    • 你可以给这个用户设置角色、分配属性,包括 tenant_id
  3. 给这个用户添加自定义属性(如 tenant_id=xxx

  4. 回到 Client 设置:

    • 菜单选择 Client Scopes > Default Client Scope
    • 添加 Mapper,把 tenant_id 属性映射进 token(可以放在 ID TokenAccess Token 中)
  5. 再次请求 Token,你应该能在 token payload 中看到 tenant_id 字段


🔧 方法二:使用 Token Exchange(Token Exchange Grant)

  • Enable Standard Token Exchange V2 for this client.

如果你需要模拟某个用户的租户上下文,可以先获取用户 Token,再通过 Token Exchange 换成 clientToken,并保留租户信息。

Standard Token Exchange

注意: 当前客户端 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 可以继承用户中的租户信息(需要配置好 ScopeMapper)。


🔧 方法三:自定义协议 Mapper(推荐)

你可以编写或配置一个 Protocol Mapper,让 clientToken 主动带上租户信息:

步骤:
  1. 进入 ClientMappers 页面
  2. 创建一个新 Mapper:
    • Name: tenant_id
    • Mapper Type: User Attribute
    • User Attribute: tenant_id
    • Token Claim Name: tenant_id
    • Claim Value Type: String
  3. 保存后,再次请求 Token,应该能看到 tenant_id 字段

📦 四、Token 示例对比

  • 默认 Tokenclient_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"
}

此时 clienttoken 就会多一个参数 tenant_id,同理其他参数也可以自定义映射(Mapper)。


✅ 五、总结

问题原因解决方案
client_credentials 获取的 Token 没有 tenant 信息client token 默认无用户上下文,不自动带租户字段配置 Service Account + Mapper
如何让 Token 包含租户信息?租户字段通常属于用户属性Service Account 用户加属性并映射
是否支持 client 级别的租户字段?支持,但需自定义 Mapper创建 User Attribute Mapper
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChaITSimpleLove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值