在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对多租户的支持主要有两种模式:
- 每个租户(tenant)使用独立的Realm,即Realm per tenant模式
- 所有租户共享同一个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"
}
可以看到:
- azp代表了当前请求access token的租户名称
- resource_access下列出了该用户所属的租户名称,以及在每个租户下所属的角色
- 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下:
之后,只需利用这些信息来实现用户授权即可。
在Keycloak中实现多租户并在ASP.NET Core下进行验证的更多相关文章
- Asp.net core下利用EF core实现从数据实现多租户(1)
前言 随着互联网的的高速发展,大多数的公司由于一开始使用的传统的硬件/软件架构,导致在业务不断发展的同时,系统也逐渐地逼近传统结构的极限. 于是,系统也急需进行结构上的升级换代. 在服务端,系统的I/ ...
- Asp.net core下利用EF core实现从数据实现多租户(3): 按Schema分离 附加:EF Migration 操作
前言 前段时间写了EF core实现多租户的文章,实现了根据数据库,数据表进行多租户数据隔离. 今天开始写按照Schema分离的文章. 其实还有一种,是通过在数据表内添加一个字段做多租户的,但是这种模 ...
- ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器)
前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...
- 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获
项目开发中的一些注意事项以及技巧总结 1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...
- Linux中以单容器部署Nginx+ASP.NET Core
引言 正如前文提到的,强烈推荐在生产环境中使用反向代理服务器转发请求到Kestrel Http服务器,本文将会实践将Nginx --->ASP.NET Core 部署架构容器化的过程. Ng ...
- Asp.net core下利用EF core实现从数据实现多租户(2) : 按表分离
前言 在上一篇文章中,我们介绍了如何根据不同的租户进行数据分离,分离的办法是一个租户一个数据库. 也提到了这种模式还是相对比较重,所以本文会介绍一种更加普遍使用的办法: 按表分离租户. 这样做的好处是 ...
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- ASP.NET Core MVC 中的 [Controller] 和 [NonController]
前言 我们知道,在 MVC 应用程序中,有一部分约定的内容.其中关于 Controller 的约定是这样的. 每个 Controller 类的名字以 Controller 结尾,并且放置在 Contr ...
- ASP.NET Core 中文文档 第三章 原理(10)依赖注入
原文:Dependency Injection 作者:Steve Smith 翻译:刘浩杨 校对:许登洋(Seay).高嵩 ASP.NET Core 的底层设计支持和使用依赖注入.ASP.NET Co ...
- 如何在ASP.NET Core中实现CORS跨域
注:下载本文的完整代码示例请访问 > How to enable CORS(Cross-origin resource sharing) in ASP.NET Core 如何在ASP.NET C ...
随机推荐
- SDL开发笔记(一):SDL介绍、编译使用以及工程模板
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- jstack查看JVM堆栈信息
目录 介绍 线程状态 Monitor 调用修饰 线程动作 命令格式 常用参数说明 使用实例 jstack pid jstack 查看线程具体在做什么,可看出哪些线程在长时间占用CPU,尽快定位问题和解 ...
- 【Azure Compute Gallery】使用 Python 代码从 Azure Compute Gallery 复制 Image-Version
问题描述 Azure Compute Gallery 可以帮助围绕 Azure 资源(例如映像和应用程序)生成结构和组织,并且支持全局复制. 如果想通过Python代码实现 Image-Version ...
- 【Azure Developer】Springboot 集成 中国区的Key Vault 报错 AADSTS90002: Tenant 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' not found
问题描述 Springboot 集成azure keyvault 报错,代码参考的官方文档:https://docs.microsoft.com/en-us/azure/developer/java/ ...
- 利用微软官方API实现Office文档的在线预览功能
随着互联网时代的飞速发展,越来越多的工作开始依赖于云端服务,我们的办公方式也逐渐发生了翻天覆地的变化.在这种背景下,急需一种无需本地安装Office软件,就能快速查看和共享Word.PowerPoin ...
- Java 线程通信的应用:经典例题:生产者/消费者问题
1 package bytezero.threadcommunication; 2 3 /** 4 * 线程通信的应用:经典例题:生产者/消费者问题 5 * 6 * 7 * 8 * @author B ...
- Java 多线程------解决 实现Runnabel接口方式线程的线程安全问题 方式二:同步方法 +总结
方式二:同步方法* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的 1 package bytezero.threadsynchronization; 2 3 4 5 /** ...
- C++ //提高编程 模板(泛型编程 STL) //模板不可以直接使用 它只是一个框架 //模板的通用并不是万能的 //语法 //template<typename T> //函数模板两种方式 //1.自动类型推导 必须推导出一致的数据类型T,才可以使用 //2.显示指定类型 模板必须确定出T的数据类型,才可以使用
1 //C++提高编程 模板(泛型编程 STL) 2 //模板不可以直接使用 它只是一个框架 3 //模板的通用并不是万能的 4 //语法 5 //template<typename T> ...
- JS4-BOM浏览器对象类型
什么是BOM 浏览器的顶级对象 页面加载事件以及注意事项 定时器函数 JS执行机制 页面跳转.刷新 history.navigator对象 什么是BOM 浏览器对象模型(Browser Object ...
- 探究C# dynamic动态类型本质
本周在做接口动态传参的时候思考了个问题:如何把一个json字符串,转成C#动态类? 比如由 { 'userId': 100, 'id': 1, 'title': 'hello world', 'com ...