使用Keycloak、Angular和SpringBoot实现多租户功能

使用Keycloak、Angular和SpringBoot实现多租户功能

解决方案goocz2025-05-07 12:49:1616A+A-


提出实施方案时,我们将提供一个用例,以便定义需求。我们将描述我们将操作的功能和技术环境,然后具体说明需求。基于这些需求,我们将提出一个Keycloak实现来满足这些需求,并在Angular和Springboot方面进行必要的调整。

环境

功能环境

这涉及一个为外部客户提供服务并雇用员工管理文件的会计事务所。如果客户(外部用户)希望连接,他们必须在Saas应用程序上创建一个帐户。同样,当员工(内部用户)希望处理文件时,他们必须使用他们的Active Directory帐户登录。

用户表示

重要的是要考虑到客户和员工可能共享一些权限,但也有不同的权限。两个数据库不得受到影响,对内部用户所做的任何更改不得影响客户。

技术环境

现有的Saas产品分为三个组件:前端、后端和数据库。

前端

这是一个Angular应用程序,负责显示信息和收集内部和外部用户输入的数据。还必须建立访问特定页面的授权。

后端

它是用SpringBoot构建的,预期从数据库中检索数据,与外部API进行接口,最重要的是管理数据访问授权。它还管理前端的配置。

数据库

存储和组织数据的PostgreSQL数据库。因此,应用程序组件将需要修改以满足这一要求。

Keycloak

身份验证将利用OpenID Connect中的OAuth2协议。Keycloak满足这些和其他要求。

架构

一个可能的解决方案是拥有两个完全独立的Keycloak实例,这可能会导致更高的维护和基础设施成本。因此,我们将调查使用单个Keycloak实例的可能性。

领域

为了逻辑上分离我们的用户,我们可以使用领域。我们将创建两个领域:内部领域:将使用UserFederation从Active Directory中提取的用户指定为内部领域。外部领域:需要在软件中创建帐户的外部用户将被指定为外部领域。

客户端

我们将在每个领域中使用两个客户端。前端客户端:这是一个不保密的公共客户端。我们将使其可用于前端组件,以获取登录页面,传输连接信息并进入应用程序。后端客户端:此客户端将是私有的,访问它将需要一个密钥。它只能被后端应用程序联系。此客户端的目的是验证前端应用程序发送的JWT令牌。

角色

角色可能不同,因为它们是特定于领域的。如果其中一些是共同的,您只需要给它们相同的名称,以便在保持组件代码领域不可知的同时引用它们。

结果

最后,我们有以下架构:\

Keycloak架构

注:角色也可以在领域级别和客户端级别实施,以获得更高的精度。

部署

为了使部署更容易,我们将使用docker-compose:

version: ’3’
services:
keycloak:
image:
quay.io/keycloak/keycloak:22.0.1
ports:
- "8080:8080"
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
command: ["start-dev"]

您可以使用'docker-compose up -d'部署您的应用程序。然后,在每个领域中创建两个领域。不需要特殊配置。然后,在每个领域中创建一个客户端-前端和客户端-后端。对于客户端-前端,您不需要修改默认领域。对于客户端-后端,您将需要将'客户端身份验证'设置为'打开'。

组件调整

现在我们已经安装和配置了Keycloak,我们需要定制组件。

前端

对于前端,我们考虑一个简单的Angular应用程序。

配置

Keycloak提出了一个javascript适配器。我们将与angular适配器一起使用它:npm install keycloakangular keycloak-js

代码调整

有了这些库,我们可以在app.module.ts中初始化应用程序时使用keycloak初始化函数,如下所示:提供者的声明和使用initializeKeycloak方法。

{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService,
KeycloakConfigService]
},

initializeKeycloak方法的声明:

export function initializeKeycloak(
    keycloak: KeycloakService,
    keycloakConfigService:
        KeycloakConfigService
) {
// 设置默认领域
    let realm = "EXTERNAL";
    const pathName: string[] =
        window.location.pathname.split('/');
    if (pathName[1] === "EXTERNAL") {
        realm = "EXTERNAL";
    }
    if (pathName[1] === "INTERNAL") {
        realm = "INTERNAL";
    }
    return (): Promise<any> => {
        return new Promise<any>(async (resolve, reject) => {
            try {
                await initMultiTenant(keycloak, keycloakConfigService, realm);
                resolve(auth);
            } catch (error) {
                reject(error);
            }
        });
    };
}

export async function initMultiTenant(
    keycloak: KeycloakService,
    keycloakConfigService:
        KeycloakConfigService,
    realm: string
) {
    return keycloak.init({
        config: {
            url: await
                firstValueFrom(keycloakConfigService
                    .fetchConfig()).then(
                    (conf: PublicKeycloakConfig) => {
                        return conf?.url;
                    }
                ),
            realm,
            clientId: 'front-client'
        },
        initOptions: {
            onLoad: 'login-required',
            checkLoginIframe: false
        },
        enableBearerInterceptor: true,
        bearerExcludedUrls:
            ['/public-configuration/keycloak']
    });
}

后端

在后端,我们应该拦截传入的请求,以便:1.获取当前领域以联系适当配置的keycloak。2.基于先前的领域,联系keycloak验证bearer令牌。

配置

为了处理Keycloak交互,我们首先需要导入Keycloak适配器和Spring安全来管理Oauth2过程:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>
 spring-boot-starter-security
 </artifactId>
</dependency>
<dependency>
 <groupId>org.keycloak</groupId>
 <artifactId>
 keycloak-spring-boot-starter
 </artifactId>
 <version>18.0.2</version>
</dependency>
<dependency>
 <groupId>org.keycloak.bom</groupId>
 <artifactId>keycloak-adapter-bom</artifactId>
 <version>18.0.2</version>
 <type>pom</type>
 <scope>import</scope>
</dependency>

代码调整

现在,我们可以拦截传入的请求以读取标头并识别请求的当前领域:

 @Override
 public KeycloakDeployment
 resolve(OIDCHttpFacade.Request request) {
 String header =
 request.getHeaders(CUSTOM_HEADER_REALM_SELECTOR)
 .stream().findFirst().orElse(null);
 if (EXT_XEN_REALM.equals(header)) {
 buildAdapterConfig(extXenKeycloakConfig);
 } else {
 buildAdapterConfig(intXenKeycloakConfig);
 }
 return
 KeycloakDeploymentBuilder.build(adapterConfig);
 }

结论

最后,我们获得以下架构:\

最终架构

  • 步骤1: 我们可以使用两个不同的URL联系我们的应用程序:
  • http://localhost:4200/external
  • http://localhost:4200/internal
  • 步骤2: 前端应用程序请求Keycloak的登录页面,使用领域作为参数,让用户在适当的登录页面上登录。
  • 步骤3: 登录页面发送回前端。
  • 步骤4: 使用keycloak-jsadatper将凭据发送到Keycloak,从而安全地传输这些敏感信息。
  • 步骤5: 如果凭据有效,返回HTTP 302以将用户重定向到前端主页。
  • 步骤6: 发送请求到后端以检索要显示的主页数据。
  • 步骤7: 拦截和解析请求以检索领域和Bearer,后端解析器联系请求的领域上的Keycloak服务器以验证Bearer 3仅当用户尚未连接时4如果无效,则返回http-401:未经授权的令牌5。
  • 步骤8: 令牌有效性发送回后端服务器。
  • 步骤9: 最后,后端可以访问请求的数据并将其发送回前端。

通过遵循这些步骤,我们可以确保用户登陆到正确的登录页面,并独立于其领域在应用程序中导航,所有这些都由单个Keycloak实例控制。

点击这里复制本文地址 以上内容由goocz整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

果子教程网 © All Rights Reserved.  蜀ICP备2024111239号-5