Using OpenID Connect (OIDC) and Keycloak to centralize authorization
了解如何在 Quarkus 应用程序中启用载体令牌授权,以 Keycloak Authorization Services 安全访问受保护的资源。
quarkus-keycloak-authorization
扩展依赖于 quarkus-oidc
。它包括一个策略执行器,用于管理对安全资源的访问。访问受 Keycloak 中设置的权限管理。目前,此扩展仅与 Quarkus OIDC service applications 兼容。
它提供了基于资源访问控制的灵活且动态的授权功能。
quarkus-keycloak-authorization
不通过基于角色的访问控制 (RBAC) 等特定机制明确执行访问,而是基于资源属性(例如名称、标识符或统一资源标识符 (URI))确定请求权限。此过程涉及将 quarkus-oidc
验证的载体访问令牌发送到 Keycloak 授权服务以进行授权决策。
仅在使用 Keycloak 且启用 Keycloak 授权服务以做出授权决策时使用 quarkus-keycloak-authorization
。如果您不使用 Keycloak 或使用 Keycloak 但未启用其 Keycloak 授权服务以做出授权决策,请使用 quarkus-oidc
。
通过将授权责任转移到应用程序外部,您可以通过各种访问控制方法来增强安全性,同时无需在安全需求不断演化时频繁地重新部署。在这种情况下,Keycloak 充当集中式授权中心,有效管理您的受保护资源及其相应权限。
有关更多信息,请参阅 OIDC Bearer token authentication 指南。重要的是要认识到,载体令牌身份验证机制进行身份验证并创建安全身份。同时, quarkus-keycloak-authorization
扩展根据当前请求路径和其他策略设置将 Keycloak 授权策略应用于此身份。
有关更多信息,请参阅 Keycloak Authorization Services documentation。
- Prerequisites
- Architecture
- Solution
- Creating the project
- Starting and configuring the Keycloak server
- Running the application in dev mode
- Running the application in JVM mode
- Running the application in native mode
- Testing the application
- Injecting the authorization client
- Mapping protected resources
- More about configuring protected resources
- Access to public resources
- Checking permission scopes programmatically
- Multi-tenancy
- Dynamic tenant configuration resolution
- Configuration reference
- References
Prerequisites
如要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
安装了 JDK 17+,已正确配置
JAVA_HOME
-
Apache Maven ${proposed-maven-version}
-
如果你想使用 Quarkus CLI, 则可以选择使用
-
如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately
Architecture
在此示例中,我们将构建一个非常简单的微服务,提供两个端点:
-
/api/users/me
-
/api/admin
这些端点受保护。仅当客户端在请求中发送一个携带令牌时才被允许访问。此令牌必须有效,具有正确的签名、到期时间和受众。此外,微服务必须信任令牌。
载体令牌由 Keycloak 服务器颁发,并代表颁发令牌的主体。作为 OAuth 2.0 授权服务器,此令牌还引用代表用户的客户端。
任何具有有效令牌的用户都可以访问 /api/users/me
端点。作为响应,它返回一个包含从载入令牌信息中获取的用户详细信息的 JSON 文档。此端点受到 RBAC 保护,并且只有被授予 user
角色的用户才能访问此端点。
/api/admin
端点受 RBAC 保护,并且只有被授予 admin
角色的用户才能访问它。
这是一个非常简单的示例,展示了如何使用 RBAC 策略来管理资源访问。但是,Keycloak 支持其他策略,您可以使用这些策略来执行更细粒度的访问控制。通过使用此示例,您会看到您的应用程序与您的授权策略完全分离,其执行完全基于访问的资源。
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案在 security-keycloak-authorization-quickstart
directory 中。
Creating the project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
此命令会生成一个项目,并导入 keycloak-authorization
扩展。此扩展为 Quarkus 应用程序实现了一个 Keycloak 适配器,并提供了与 Keycloak 服务器集成和执行持有者令牌授权所需的所有功能。
如果您已经配置了 Quarkus 项目,则可以通过在项目基础目录中运行以下命令来将 oidc
和 keycloak-authorization
扩展添加到您的项目:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下依赖添加到您的构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
implementation("io.quarkus:quarkus-oidc")
implementation("io.quarkus:quarkus-keycloak-authorization")
让我们从实现 /api/users/me
端点开始。如您在以下源代码中看到的那样,它是一个常规的 Jakarta REST 资源:
package org.acme.security.keycloak.authorization;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.NoCache;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/api/users")
public class UsersResource {
@Inject
SecurityIdentity identity;
@GET
@Path("/me")
@NoCache
public User me() {
return new User(identity);
}
public static class User {
private final String userName;
User(SecurityIdentity identity) {
this.userName = identity.getPrincipal().getName();
}
public String getUserName() {
return userName;
}
}
}
/api/admin
端点的源代码也很简单:
package org.acme.security.keycloak.authorization;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import io.quarkus.security.Authenticated;
@Path("/api/admin")
@Authenticated
public class AdminResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "granted";
}
}
请注意,我们没有定义诸如 @RolesAllowed
之类的注解来明确执行对资源的访问。相反,该扩展负责映射 Keycloak 中受保护资源的 URI,并相应地评估权限,根据 Keycloak 授予的权限授予或拒绝访问。
Configuring the application
OpenID Connect 扩展允许您使用 application.properties
文件定义适配器配置,该文件通常位于 src/main/resources
目录中。
# OIDC Configuration
%prod.quarkus.oidc.auth-server-url=https://localhost:8543/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret
quarkus.oidc.tls.verification=none
# Enable Policy Enforcement
quarkus.keycloak.policy-enforcer.enable=true
# Tell Dev Services for Keycloak to import the realm file
# This property is not effective when running the application in JVM or native modes
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
为 |
默认情况下,使用 |
Starting and configuring the Keycloak server
当您在开发模式下运行应用程序时,不要启动 Keycloak 服务器。Keycloak 开发服务会启动一个容器。有关更多信息,请参见 Running the application in Dev mode 部分。 |
要启动 Keycloak 服务器,请使用以下 Docker 命令:
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8543:8443 -v "$(pwd)"/config/keycloak-keystore.jks:/etc/keycloak-keystore.jks quay.io/keycloak/keycloak:{keycloak.version} start --hostname-strict=false --https-key-store-file=/etc/keycloak-keystore.jks
其中 keycloak.version
必须是 25.0.2
或更高版本,而 keycloak-keystore.jks
可在 quarkus-quickstarts/security-keycloak-authorization-quickstart/config 中找到。
尝试通过 localhost:8543 访问您的 Keycloak 服务器。
要访问 Keycloak 管理控制台,请使用 admin
用户登录。用户名和密码均为 admin
。
导入 realm configuration file 以创建新领域。有关更多详细信息,请参阅 Keycloak 文档,了解如何 create a new realm。
导入领域后,您可以看到资源权限:
它解释了为什么端点没有 @RolesAllowed
注释 - 资源访问权限直接设置在 Keycloak 中。
Running the application in dev mode
要以 dev 模式运行应用程序,请使用:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Dev Services for Keycloak 启动 Keycloak 容器并导入 quarkus-realm.json
.
当被要求登录到由 OpenID Connect Dev UI
提供的 Single Page Application
时:
-
以
alice
(密码:alice
) 登录,他只有权访问User Permission
/api/users/me
资源:-
访问
/api/admin
,它将返回403
。 -
访问
/api/users/me
,它将返回200
。
-
-
注销并以
admin
(密码:admin
) 登录,他既可以访问Admin Permission
/api/admin
资源,也可以访问User Permission
/api/users/me
资源:-
访问
/api/admin
,它将返回200
。 -
访问
/api/users/me
,它将返回200
。
-
如果您在不导入诸如 quarkus-realm.json 的已配置为支持 Keycloak Authorization 的领域文件的情况下启动 Dev Services for Keycloak,请创建一个没有 Keycloak 授权策略的默认 quarkus
领域。在这种情况下,您必须在 OpenId Connect
Dev UI 卡中选择 Keycloak Admin
链接并在默认 quarkus
领域中配置 Keycloak Authorization Services。
在 Dev UI 中,很容易找到 Keycloak Admin
链接:
登录到 Keycloak 管理控制台时,用户名和密码均为 admin
。
如果您的应用程序使用已配置为 JavaScript policies 的 Keycloak 授权,并部署在 JAR 文件中,则可以设置 Keycloak 的开发服务,将此档案传输到 Keycloak 容器。例如:
quarkus.keycloak.devservices.resource-aliases.policies=/policies.jar 1
quarkus.keycloak.devservices.resource-mappings.policies=/opt/keycloak/providers/policies.jar 2
1 | 为 /policies.jar classpath 资源创建 policies 别名。策略档案也可以位于文件系统中。 |
2 | 策略档案映射到 /opt/keycloak/providers/policies.jar 容器位置。 |
Running the application in JVM mode
在 dev 模式下探索应用程序后,您可以将其作为标准 Java 应用程序运行。
首先编译它:
quarkus build
./mvnw install
./gradlew build
然后运行它:
java -jar target/quarkus-app/quarkus-run.jar
Running the application in native mode
可以将相同的演示编译成本机代码,无需改动。
这意味着您不再需要在您的生产环境中安装 JVM,因为运行时技术包含在生产的二进制文件中并在最低限度资源下运行。
编译需要较长时间,所以默认情况下此步骤被关闭;让我们通过启用`native`配置文件再次构建:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
一段时间后,您可以直接运行此二进制文件:
./target/security-keycloak-authorization-quickstart-runner
Testing the application
请参阅上文Running the application in Dev mode关于在 dev 模式中测试您的应用程序的部分。
您可以使用`curl`测试在 JVM 或本地模式中启动的应用程序。
该应用程序使用 bearer 标记授权,首先要从 Keycloak 服务器获取一个访问标记以访问应用程序资源:
export access_token=$(\
curl --insecure -X POST https://localhost:8543/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
)
上文示例为用户`alice`获取一个访问标记。
任何用户都可以访问`http://localhost:8080/api/users/me`端点,它会返回一个 JSON 有效负载,包含有关该用户的详细信息。
curl -v -X GET \
http://localhost:8080/api/users/me \
-H "Authorization: Bearer "$access_token
只有具备`admin`角色的用户才能访问`http://localhost:8080/api/admin`端点。如果您尝试使用先前发出的访问令牌访问此端点,您会从服务器处得到一个`403`响应。
curl -v -X GET \
http://localhost:8080/api/admin \
-H "Authorization: Bearer "$access_token
要访问 admin 端点,请获取一个`admin`用户的令牌:
export access_token=$(\
curl --insecure -X POST https://localhost:8543/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
)
Injecting the authorization client
在某些情况下,使用 Keycloak Authorization Client Java API对于管理资源和直接从 Keycloak 获取权限等任务很有益。为此,您可以将`AuthzClient`实例注入到您的 bean 中,如下所示:
public class ProtectedResource {
@Inject
AuthzClient authzClient;
}
如果您想要直接使用`AuthzClient`,请设置`quarkus.keycloak.policy-enforcer.enable=true`;否则,没有可用于注入的 bean。 |
Mapping protected resources
默认情况下,该扩展程序按需从 Keycloak 获取资源,使用它们的 URI 来识别和映射需要保护的您应用程序中的资源。
要禁用按需获取并预先加载资源,请应用如下配置设置:
quarkus.keycloak.policy-enforcer.lazy-load-paths=false
在启动时从 Keycloak 预先加载资源所需的时间会根据资源的数量而异,可能会影响您应用程序的初始加载时间。
More about configuring protected resources
在默认配置中,Keycloak 管理角色并决定哪些人可以访问哪些路由。
要使用`@RolesAllowed`注解或`application.properties`文件来配置受保护路由,请检查OpenID Connect (OIDC) Bearer token authentication和Authorization of web endpoints指南。要了解更多详细信息,请查看Quarkus Security overview。
Access to public resources
要在不受`quarkus-keycloak-authorization`应用其政策的情况下启用对公共资源的访问,请在`application.properties`中创建一个`permit`HTTP 策略配置。要了解更多信息,请参阅Authorization of web endpoints指南。
不需要使用此类设置注销 Keycloak 授权策略的政策检查:
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/public
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=DISABLED
若要阻止匿名用户访问公用资源,您可以创建强制实施的 Keycloak 授权策略:
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/public-enforcing
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=ENFORCING
如果您需要控制匿名用户访问公用资源,则仅适用于默认租户配置。
Checking permission scopes programmatically
除了资源权限外,您可以指定方法作用域。作用域通常表示可以对资源执行的操作。您可以创建一个具有方法作用域的实施强制 Keycloak 授权策略。例如:
# path policy with enforced scope 'read' for method 'GET'
quarkus.keycloak.policy-enforcer.paths.1.name=Scope Permission Resource
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/protected/standard-way
quarkus.keycloak.policy-enforcer.paths.1.methods.get.method=GET
quarkus.keycloak.policy-enforcer.paths.1.methods.get.scopes=read 1
# path policies without scope
quarkus.keycloak.policy-enforcer.paths.2.name=Scope Permission Resource
quarkus.keycloak.policy-enforcer.paths.2.paths=/api/protected/programmatic-way,/api/protected/annotation-way
1 | 用户必须拥有资源权限“Scope Permission Resource”和作用域“read” |
Keycloak 策略强制程序现在保护 /api/protected/standard-way
请求路径,无需其他注释,例如 @RolesAllowed
。然而,在某些情况下,需要程序检查。您可以通过将 SecurityIdentity
实例注入到 bean 中来实现此目的,如以下示例所示。或者,您可以通过使用 @PermissionsAllowed
注释资源方法来获得相同的结果。以下示例演示了三个资源方法,每个方法都要求相同的 read
作用域:
import java.security.BasicPermission;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.keycloak.representations.idm.authorization.Permission;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;
@Path("/api/protected")
public class ProtectedResource {
@Inject
SecurityIdentity identity;
@GET
@Path("/standard-way")
public Uni<List<Permission>> standardWay() { 1
return Uni.createFrom().item(identity.<List<Permission>> getAttribute("permissions"));
}
@GET
@Path("/programmatic-way")
public Uni<List<Permission>> programmaticWay() {
var requiredPermission = new BasicPermission("Scope Permission Resource") {
@Override
public String getActions() {
return "read";
}
};
return identity.checkPermission(requiredPermission).onItem() 2
.transform(granted -> {
if (granted) {
return identity.getAttribute("permissions");
}
throw new ForbiddenException();
});
}
@PermissionsAllowed("Scope Permission Resource:read") 3
@GET
@Path("/annotation-way")
public Uni<List<Permission>> annotationWay() {
return Uni.createFrom().item(identity.<List<Permission>> getAttribute("permissions"));
}
}
1 | 请求子路径 /standard-way 根据我们先前在 application.properties 中设置的配置属性,同时要求资源权限和作用域 read 。 |
2 | 请求子路径 /programmatic-way 只需要权限 Scope Permission Resource ,但我们可以通过 SecurityIdentity#checkPermission 来实施作用域。 |
3 | @PermissionsAllowed 注释仅授予对具有权限 Scope Permission Resource 和作用域 read 的请求的访问权限。有关更多信息,请参阅安全授权指南的 Authorization using annotations 部分。 |
Multi-tenancy
您可以为每个租户设置策略强制程序配置,这与通过 OpenID Connect (OIDC) multi-tenancy 完成的方式类似。
例如:
quarkus.keycloak.policy-enforcer.enable=true
# Default Tenant
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret
quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.policy-enforcer.paths.1.name=Permission Resource
quarkus.keycloak.policy-enforcer.paths.1.paths=/api/permission
quarkus.keycloak.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim
# Service Tenant
quarkus.oidc.service-tenant.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus
quarkus.oidc.service-tenant.client-id=quarkus-app
quarkus.oidc.service-tenant.credentials.secret=secret
quarkus.keycloak.service-tenant.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.name=Permission Resource Service
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.paths=/api/permission
quarkus.keycloak.service-tenant.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim
# WebApp Tenant
quarkus.oidc.webapp-tenant.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus
quarkus.oidc.webapp-tenant.client-id=quarkus-app
quarkus.oidc.webapp-tenant.credentials.secret=secret
quarkus.oidc.webapp-tenant.application-type=web-app
quarkus.oidc.webapp-tenant.roles.source=accesstoken
quarkus.keycloak.webapp-tenant.policy-enforcer.enforcement-mode=PERMISSIVE
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.name=Permission Resource WebApp
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.paths=/api/permission
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim
Dynamic tenant configuration resolution
如果您需要对您想要支持的不同租户采用更动态的配置,并且不希望最终在您的配置文件中有多个条目,则可以使用 io.quarkus.keycloak.pep.TenantPolicyConfigResolver
。
此界面允许您在运行时动态创建租户配置:
package org.acme.security.keycloak.authorization;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.keycloak.pep.TenantPolicyConfigResolver;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerConfig;
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomTenantPolicyConfigResolver implements TenantPolicyConfigResolver {
private final KeycloakPolicyEnforcerTenantConfig enhancedTenantConfig;
private final KeycloakPolicyEnforcerTenantConfig newTenantConfig;
public CustomTenantPolicyConfigResolver(KeycloakPolicyEnforcerConfig enforcerConfig) {
this.enhancedTenantConfig = KeycloakPolicyEnforcerTenantConfig.builder(config) 1
.paths("/enhanced-config")
.permissionName("Permission Name")
.get("read-scope")
.build();
this.newTenantConfig = KeycloakPolicyEnforcerTenantConfig.builder() 2
.paths("/new-config")
.claimInformationPoint(Map.of("claims", Map.of("grant", "{request.parameter['grant']}")))
.build();
}
@Override
public Uni<KeycloakPolicyEnforcerTenantConfig> resolve(RoutingContext routingContext, OidcTenantConfig tenantConfig,
OidcRequestContext<KeycloakPolicyEnforcerTenantConfig> requestContext) {
String path = routingContext.normalizedPath();
String tenantId = tenantConfig.tenantId.orElse(null);
if ("enhanced-config-tenant".equals(tenantId) && path.equals("/enhanced-config")) {
return Uni.createFrom().item(enhancedTenantConfig);
} else if ("new-config-tenant".equals(tenantId) && path.equals("/new-config")) {
return Uni.createFrom().item(newTenantConfig);
}
return Uni.createFrom().nullItem(); 3
}
}
1 | 在默认租户配置中创建或更新 /enhanced-config 路径。 |
2 | 将 /new-config 路径添加到填充有记录的配置默认值的租户配置中。 |
3 | 使用基于 application.properties 文件和其他 SmallRye Config 配置源的默认静态租户配置解析。 |
Configuration reference
此配置遵循官方 [Keycloak 策略强制程序配置]([role="bare"][role="bare"]https://www.keycloak.org/docs/latest/authorization_services/index.html#_enforcer_filter) 指南。有关各种配置选项的详细信息,请参阅以下文档:
Unresolved include directive in modules/ROOT/pages/security-keycloak-authorization.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-keycloak-authorization.adoc[]