使用Keycloak、Angular和SpringBoot实现多租户功能
提出实施方案时,我们将提供一个用例,以便定义需求。我们将描述我们将操作的功能和技术环境,然后具体说明需求。基于这些需求,我们将提出一个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实例控制。