在Keycloak中实现多租户并在ASP.NET Core下进行验证

Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用户身份管理和访问控制的场景。

SaaS(Software as a Service,软件即服务)是一种软件分发模式,其中软件应用程序通过互联网托管并由服务提供商管理。用户通常通过订阅模式访问这些服务,而不是购买和安装软件。SaaS应用程序通常部署在云环境中,只要用户有互联网连接,他们就可以随时随地访问服务。在SaaS应用中,多租户支持是关键需求之一。在多租户环境中,一个服务实例需要为多个租户(即不同的公司、组织或个人用户)提供服务,同时保障数据隔离和安全性。因此,多租户的支持也成为了与SaaS应用集成的身份和访问管理(IAM)服务的基本需求,在选用Keycloak作为SaaS应用IAM服务的场景下,对于多租户模式的支持,也是Keycloak实施的一个关键需求。

Keycloak中的基本概念及其之间的关系

要理解Keycloak对于多租户的支持方式,首先需要了解Keycloak中的一些基本概念以及它们之间的关系。当然,本文不会详细解释这些概念是什么,Keycloak的官方文档里有详细的解释,了解这部分内容将有助于选择以何种方式实现多租户。

下图简要描述了Keycloak中的一些主要的基本概念及其之间的关系:

在一个Keycloak的部署中包含多个Realm,Realm是一个管理的域,它作为顶层组织单元,用来封装和管理一组用户、角色、群组和客户端(即应用程序或服务)。每个Realm可以有自己的一套用户目录、身份验证和授权规则、客户端配置以及其他安全性设置,不同的Realm之间是相互隔离的。在一个Realm中,可以创建多个Client,Client是一个非常重要的概念,它代表了可以请求Keycloak进行认证和授权的实体。在OAuth 2.0和OpenID Connect(OIDC)的上下文中,客户端通常指的是一个应用程序,它希望使用Keycloak作为身份提供者来验证用户的身份,并可能获取用户授权的令牌,以便访问受保护的资源。在不同的Client下,可以创建互相隔离的一组Client Role,也就是Client下的角色,一个角色可以关联到多个用户(user)或者用户组(group)上。与此同时,Client下还可以有一组Client Scope,而Client Scope还可以映射到某个或者某些用户信息属性(user profile attribute)上,而用户信息属性(user profile attribute)则可以是定义在用户(user)上的自定义属性。

Keycloak中多租户的不同模式

从上面的结构可以分析得出,Keycloak对多租户的支持主要有两种模式:

  1. 每个租户(tenant)使用独立的Realm,即Realm per tenant模式
  2. 所有租户共享同一个Realm,但使用不同的Client,即Single Realm模式

需要根据不同需求来选择不同的模式。

Realm per Tenant

这种模式的实现比较简单清晰:数据严格隔离,简单地按照上图中的关系对Realm进行配置就行了,比如,在Realm下可以直接创建用户组,这些用户组就是属于当前这个租户的;但另一方面,由于不同的Realm拥有完全不同的一套用户(user)和用户组(group)数据,于是,Realm per Tenant无法实现某个用户同时属于多个租户的场景。你可以在不同的租户中创建名字和电子邮件地址完全相同的用户,但是,它们虽然名字相同,却是完全不同的两个实体。除此之外,当租户数量越来越大时,Realm的个数也会相应增加,对于Keycloak来说也存在一定的性能影响(我没有实际测试过,但是根据目前来自各方面的文档资料,Keycloak维护上百个Realm还是没有太大问题的)。

Single Realm

在Single Realm模式下,每个租户就是一个Client,好处是用户及用户组可以属于多个租户,比如:A用户可以同时属于T1和T2两个租户,也不必考虑Realm太多影响性能。每个Client都可以配置不同的登录选项,因此,可以满足多租户的需求。但实现这种模式相对比较复杂,而且有些信息比如用户的角色、所属的用户组等会全部出现在用户的access token里,而这些信息有些是属于特定租户的,其数据隔离性没有Realm per Tenant模式好。

使用Single Realm实现多租户的一般思路是,针对每一个租户创建一个Client,所以在这个Realm下,用户是跨租户的,用户组理论上也是跨租户的,但是,可以对不同的租户,设置不同的用户组,然后在这个租户级别的用户组下,还可以创建子组,所以,用户组也可以做到按租户隔离。对于角色而言,Client下本身就可以分配不同的角色,所要做的就是将用户/用户组分配到不同的角色上,只不过选择角色的时候需要指定,是分配到哪个Client下的哪个角色。

演练:在Keycloak中配置Single Realm多租户模式

注意:本文演练部分基于Keycloak 24.0.2,对于其它版本的Keycloak,界面操作可能会有出入。

创建一个新的Realm

启动Keycloak本地开发环境,推荐使用docker,可以参考这篇文章。启动成功后,用admin/admin登录管理界面,点击界面左边的Keycloak下拉框,然后点击Create realm按钮,以创建一个新的Realm。

在Create realm界面,只需要输入Realm name即可,例如:multitenant,然后点击Create按钮,系统提示Realm created successfully,表示Realm创建成功。

创建对应于租户的Client

接下来,在侧边栏中点击Clients链接,打开Clients列表界面,在界面中,点击Create client,注意:这里我们就是为某个租户创建一个Client,因此,可以考虑用租户的名称来命名这个Client,假设我们客户的名字叫Globex Corporation,于是,Client就命名为globex。在Create client界面中,Client ID填globex,然后点击Next按钮:

在Capability config下,启用Client authentication,Authorization也可以考虑启用,在Authentication flow中,暂时先勾选Direct access grants,以便后面的测试。在OIDC的Authorization Code Flow和Authorization Code Flow /w PKCE两种flow下,该选项可以关闭,当然,具体的可以根据项目实际情况选择。这部分的配置如下:

点击Next按钮,在Login settings页面,可以无需修改配置,直接点击Save按钮,此时Client创建成功。

配置Client scopes

在左侧侧边栏,点击Client scopes,然后点击Create client scope按钮,在Create client scope页面中,输入Client scope名称,其它的默认就行,然后点击Save按钮:

在新创建的Client scope下,点击Mappers标签页,然后点击Configure a new mapper按钮:

在Configure a new mapper列表中,找到Group Membership,直接点击选中,然后在Add mapper页面下,输入Mapper的名字,然后Token Claim Name也指定一下,其它的开关按钮保持默认就行。这里注意的是,Token Claim Name并不是必须的,但是如果你希望它出现在用户的access token中,那么这个是必填的:

完成配置后点击Save按钮保存配置。然后回到Clients列表,点击刚刚新建的Client:globex,进入Client scopes标签页,单击Add client scope按钮:

在弹出的Add client scopes to globex对话框中,选择刚刚新建的Client scope,然后点击Add -> Default按钮:

配置用户(user)和用户组(group)

注意:此处配置的用户和用户组是跨多租户,也就是跨多个Client的,之后会在Client的Role下通过角色关联来将用户关联到租户上。

在左边侧边栏点击Users链接,在Users界面中,点击Create new user按钮,然后在General部分输入Username,其它信息可以按需填写,然后点击Create按钮:

在用户详细信息页面中,点击Credentials选项卡,然后点击Set password按钮:

在弹出的对话框中,输入密码,然后关闭Temporary选项,再点击Save按钮,在确认对话框中,点击红色的Save password按钮即可:

在左侧侧边栏点击Groups链接,然后点击Create group按钮,在弹出的Create a group对话框中,输入组名。由于我们希望不同租户具有不同的组配置策略,也就是租户的用户组也需要隔离,因此,在这里组名就使用租户的名称,也就是globex。然后点击Create按钮创建组。在成功创建组之后,点击Members选项卡,然后点击Add member按钮,在弹出的Add member对话框中,将刚刚新建的super用户添加到globex组中。

一个对应于租户的组(比如这里的globex组)下还可以创建多个子用户组,也就可以定义针对不同租户的用户组层次结构。

配置用户/用户组角色

点击左边侧边栏的Clients链接,在Clients列表中,点击globex Client,在Roles选项卡下,点击Create role按钮,新建一个角色,比如administrator,然后点击Save按钮:

回到Users或者Groups界面,选择某个用户或者用户组,比如选择刚刚新建的super用户,在用户的Role mapping选项卡下,点击Assign role按钮:

在Assign roles to super对话框中,点击Filter by realm roles下拉框,选择Filter by clients,然后选择globex这个Client的administrator角色,表示需要将globex租户的administrator角色分配给super用户:

测试我们的配置

点击左侧侧边栏的Clients链接,然后选择globex Client,进入Credentials选项卡,在Client Secret部分点击复制按钮,将Client Secret复制到剪贴板中,然后打开Postman,使用下面的HTTP请求:

  • POST URL为http://<server>:<port>/realms/<realm>/protocol/openid-connect/token
  • grant_type:password
  • client_id:租户的名称,globex
  • client_secret:上一步复制到剪贴板的内容
  • password:用户密码
  • username:用户名称

点击Send按钮后,即可得到access token。打开https://jwt.io,将access token复制到Debugger的Encoded部分,即可解出access token的明文:

详细内容类似如下:

{
  "exp": 1712410272,
  "iat": 1712409972,
  "jti": "8f18192e-c223-4f60-a23d-6e825f5e6b37",
  "iss": "http://localhost:5600/realms/multitenant",
  "aud": "account",
  "sub": "1a666edc-36c2-4704-ac14-2a8d88332781",
  "typ": "Bearer",
  "azp": "globex",
  "session_state": "448c536c-3f11-4275-b03c-78bec00deb56",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization",
      "default-roles-multitenant"
    ]
  },
  "resource_access": {
    "globex": {
      "roles": [
        "administrator"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "email profile",
  "sid": "448c536c-3f11-4275-b03c-78bec00deb56",
  "email_verified": false,
  "name": "Super Admin",
  "groups": [
    "/globex"
  ],
  "preferred_username": "super",
  "given_name": "Super",
  "family_name": "Admin",
  "email": "super@globex.com"
}

可以看到:

  1. azp代表了当前请求access token的租户名称
  2. resource_access下列出了该用户所属的租户名称,以及在每个租户下所属的角色
  3. groups下列出了该用户所属的用户组,很遗憾,这里它会将所有该用户所属的用户组列出来,比如,如果还存在另一个租户soylent,那么,这个groups数组里就会出现/soylent这个项目。在实际项目中,可以在Action Filter中,通过代码来处理这个信息

与ASP.NET Core Web API的集成

此时可以新建一个ASP.NET Core Web API应用程序,集成Keycloak身份认证,来查看在User Principle上可以拿到哪些数据。修改Program.cs文件,加入这些代码:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "http://localhost:5600/realms/multitenant";
        options.MetadataAddress = 
            "http://localhost:5600/realms/multitenant/.well-known/openid-configuration";
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            NameClaimType = "preferred_username",
            RoleClaimType = ClaimTypes.Role,
            ValidateIssuer = true,
            ValidateAudience = false
        };
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

然后,在Controller上加入[Authorize]特性:

[ApiController]
[Authorize]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
  // 省略N行
}

在某个Controller Action方法上设置一个调试断点,然后启动ASP.NET Core Web API应用程序。接下来,在Postman中调用这个设置了调试断点的API服务,记得Authorization选择Bearer Token,然后将上面通过Postman获得的access_token作为Bearer Token填入,发起请求。此时,Visual Studio中断点会被命中,查看User对象,可以看到,有关租户的一些信息已经在User的Claims下:

之后,只需利用这些信息来实现用户授权即可。

原文地址:https://www.cnblogs.com/daxnet/p/18115003

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便