使用OpenID连接(OIDC)与Keycloak来做集中式授权
本指南展示了如何在您的Quarkus应用程序中使用 Keycloak授权服务 来授权不记名token(bearer token)访问受保护的资源。
quarkus-keycloak-authorization
扩展基于 quarkus-oidc
,并提供了一个策略实施者(policy enforcer)来根据Keycloak管理的权限去实施对受保护资源的访问。它目前只能与Quarkus OIDC服务应用 一起使用。它提供了一个基于资源访问控制的灵活和动态的授权能力。换句话说,您不需要明确地根据一些特定的访问控制机制(例如:RBAC)来执行访问,而只需要根据资源的名称、标识或URI来检查一个请求是否被允许访问。
通过将授权功能从您的应用程序中抽离并外部化,您可以使用不同的访问控制机制来保护您的应用程序,也可以避免在您的安全控制需求发生变化时重新部署您的应用程序。Keycloak将作为一个集中的授权服务,它允许您的受保护资源和它们的相关权限在它这里得到管理。
关于 Bearer Token
认证机制的更多信息,请参见 使用OpenID Connect保护服务应用 指南。重要的是,是 Bearer Token
认证机制来进行认证并创建一个安全身份 - 同时 quarkus-keycloak-authorization
扩展负责根据当前的请求路径和其他策略设置对该身份来应用Keycloak授权策略。
如果您已经熟悉Keycloak,您会注意到这个扩展基本上是另一个专门针对Quarkus的keycloak适配器的实现。另外,您也可以在Keycloak 文档 中找到更多信息。
先决条件
要完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
安装JDK 11以上版本并正确配置了
JAVA_HOME
-
Apache Maven 3.8.1+
-
A working container runtime (Docker or Podman)
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
架构
在该例子中,我们建立了一个非常简单的微服务并提供了两个节点:
-
/api/users/me
-
/api/admin
这些节点是受保护的,只有当客户端将一个bearer token与请求一起发送时才能被访问,而且该token必须是有效的(例如:签名、过期时间和受众),并且要得到微服务的信任。
Bearer token是由Keycloak服务器发放的,而且代表了该token的发放对象。对于作为OAuth 2.0授权服务器,该token还可以表示代表用户的客户端。
/api/users/me
节点可由任何拥有有效token的用户访问。作为响应,它返回一个JSON文档,其中包含关于用户的详细信息,这些详细信息是从token上的信息中获得的。这个节点受到RBAC(基于角色的访问控制)的保护,只有被授予 user
角色的用户才能访问这个节点。
/api/admin
节点受到RBAC(基于角色的访问控制)的保护,只有被授予 admin
角色的用户才能访问它。
该示例非常简单,它使用RBAC策略来管理对您的资源的访问。但是,Keycloak也支持其他类型的策略,您可以用这些策略来执行更细粒度的访问控制。通过这个例子,您会发现您的应用程序与授权策略完全解耦,授权策略的执行完全基于所访问的资源。
解决方案
我们建议您按照下面几节的说明,一步一步地创建应用程序。但您也可以直接跳到已完成的例子。
克隆 Git 仓库。 git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或者下载一个 存档 。
该方案位于 security-keycloak-authorization-quickstart
目录中。
创建项目
首先,我们需要一个新的工程项目。用以下命令创建一个新项目:
该命令生成一个项目并导入 keycloak-authorization
扩展,该扩展是一个用于Quarkus应用程序的Keycloak适配器的实现,并提供所有必要的功能来与Keycloak服务器集成并执行bearer token授权。
如果您已经配置了Quarkus项目,您可以通过在项目根目录中执行以下命令将 oidc
和 keycloak-authorization
扩展添加到您的项目中:
quarkus extension add 'oidc,keycloak-authorization'
./mvnw quarkus:add-extension -Dextensions="oidc,keycloak-authorization"
./gradlew addExtension --extensions="oidc,keycloak-authorization"
这将在您的构建文件中添加以下内容:
<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
节点开始。从下面的源代码可以看出,它就是一个普通的JAX-RS资源:
package org.acme.security.keycloak.authorization;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import org.jboss.resteasy.annotations.cache.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 javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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授予的权限来授予或拒绝访问。
配置应用
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
在 quarkus.oidc.auth-server-url 上添加一个 %prod. profile前缀,可以确保 Keycloak开发服务 在应用程序以开发模式运行时为您启动一个容器。更多信息请参见下面的 " 在开发模式下运行应用程序 "部分。
|
默认情况下,使用 quarkus-oidc 扩展的应用程序被标记为 service 类型的应用程序(见 quarkus.oidc.application-type )。这个扩展也仅支持 web-app 类型的应用程序且只有当作为授权码授予响应的一部分而返回的访问token被标记为角色的来源时可行: quarkus.oidc.roles.source=accesstoken ( web-app 类型的应用程序会默认检查ID token角色)。
|
启动和配置Keycloak服务器
当您以开发模式运行应用程序时,请不要启动Keycloak服务器 - Keycloak开发服务 将启动一个容器。更多信息请参见下面在 开发模式下运行应用程序 的部分。
|
要启动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
应该设置为 17.0.0
或更高。
现在您应该可以在 localhost:8543 访问您的Keycloak服务器了。
以 admin
用户身份登录,访问Keycloak管理控制台。管理员账户请使用 admin
,密码是 admin
。
导入https://github.com/quarkusio/quarkus-quickstarts/tree/main/security-keycloak-authorization-quickstart/config/quarkus-realm.json[realm配置文件]来创建一个新realm。更多细节,请参阅Keycloak文档中关于如何 创建一个新的realm 。
导入realm后,您可以看到资源的权限:

这解释了为什么节点没有 @RolesAllowed
注解—资源访问权限是直接在Keycloak中设置的。
在开发模式下运行应用程序
要在开发模式下运行应用程序,请使用:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
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
-
在JVM模式下运行应用程序
当您尝试完毕 开发
模式后,您可以把它作为一个标准的Java应用程序运行。
首先编译它:
quarkus build
./mvnw clean package
./gradlew build
然后运行它:
java -jar target/quarkus-app/quarkus-run.jar
在本地模式下运行应用程序
这个同样的演示可以被编译成本地代码:不需要任何修改。
这意味着您不再需要在您的生产环境中安装JVM,因为运行时技术包含在生产的二进制文件中,并以最小的资源开销优化运行。
编译会花一点时间,所以这一步默认是禁用的;让我们通过启用 native
profile来再次构建:
quarkus build --native
./mvnw package -Dnative
./gradlew build -Dquarkus.package.type=native
一杯咖啡的时间后,您就可以直接运行该二进制文件了:
./target/security-keycloak-authorization-quickstart-runner
测试应用程序
关于在开发模式下测试您的应用程序,请参见上面 在开发模式下运行应用程序 一节。
您可以用 curl
测试在JVM或Native模式下启动的应用程序。
该应用程序使用bearer token授权,首先要做的是从Keycloak服务器获得一个访问token,以便访问应用程序资源:
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
获取了一个访问token。
任何用户都被允许访问 <a href="http://localhost:8080/api/users/me" class="bare">http://localhost:8080/api/users/me</a>
节点,该节点会返回一个包含用户详细信息的JSON payload。
curl -v -X GET \
http://localhost:8080/api/users/me \
-H "Authorization: Bearer "$access_token
<a href="http://localhost:8080/api/admin" class="bare">http://localhost:8080/api/admin</a>
节点只能由具有 admin
角色的用户访问。如果您试图用先前发放的access token访问这个节点,您将会得到一个 403
的响应。
curl -v -X GET \
http://localhost:8080/api/admin \
-H "Authorization: Bearer "$access_token
为了访问管理节点,您需要获得一个 admin
用户的token:
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' \
)
以编程方式检查权限
在某些情况下,您可能想以编程方式检查一个请求是否被允许访问受保护的资源。通过在您的Bean中注入一个 SecurityIdentity
实例,您就可以按以下方式检查权限了:
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;
@Path("/api/protected")
public class ProtectedResource {
@Inject
SecurityIdentity identity;
@GET
public Uni<List<Permission>> get() {
return identity.checkPermission(new AuthPermission("{resource_name}")).onItem()
.transform(granted -> {
if (granted) {
return identity.getAttribute("permissions");
}
throw new ForbiddenException();
});
}
}
注入授权客户端
在某些情况下,您可能想使用 Keycloak授权客户端Java API 来执行特定的操作,如管理资源和直接从Keycloak获得权限。为此,您可以按以下方式将一个 AuthzClient
实例注入到您的bean中:
public class ProtectedResource {
@Inject
AuthzClient authzClient;
}
注意:如果您想直接使用 AuthzClient
,请确保设置 quarkus.keycloak.policy-enforcer.enable=true
,否则将没有Bean可供注入。
映射受保护资源
默认情况下,扩展将从Keycloak按需获取资源,而资源 URI
被用于映射您的应用程序中应被保护的资源。
如果您想禁用这种行为,并在启动期间获取资源,您可以使用以下配置:
quarkus.keycloak.policy-enforcer.lazy-load-paths=false
请注意,基于您在Keycloak中的资源数量,获取这些资源的时间可能会影响您的应用程序的启动时间。
关于配置受保护资源的更多信息
在默认配置中,Keycloak负责管理角色并决定谁可以访问哪些路由。
要使用 @RolesAllowed
注解或 application.properties
文件配置受保护的路由,请查看《 使用 OpenID 连接适配器保护 JAX-RS 应用程序 》和《 安全授权 》指南。更多细节,请查看 安全指南
获取公共资源
如果您想访问一个不受 quarkus-keycloak-authorization
授权策略影响的公共资源,那么您需要在 application.properties
中创建一个 permit
HTTP策略配置,正如 安全授权 指南中所记载的那样。
使用Keycloak授权策略禁用一个策略检查,如:
quarkus.keycloak.policy-enforcer.paths.1.path=/api/public
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=DISABLED
is no longer required.
如果您想阻止匿名用户访问公共资源,那么您可以创建一个强制的Keycloak授权策略:
quarkus.keycloak.policy-enforcer.paths.1.path=/api/public-enforcing
quarkus.keycloak.policy-enforcer.paths.1.enforcement-mode=ENFORCING
注意只有在需要控制公共资源的匿名访问时才适用默认租户配置。
多租户
可以配置多个策略执行者的配置,每个租户一个,类似于 多租户OpenID连接服务应用程序 的配置方式。
例如:
quarkus.keycloak.policy-enforcer.enable=true
# Default Tenant
quarkus.oidc.auth-server-url=${keycloak.url}/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.path=/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}/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.path=/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}/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.path=/api/permission
quarkus.keycloak.webapp-tenant.policy-enforcer.paths.1.claim-information-point.claims.static-claim=static-claim
配置参考
以下配置基于官方文档 Keycloak Policy Enforcer Configuration。如果您想要查看不同配置选项的更多细节,请参看该文档,
以下配置选项在构建中会被确定下来 - 其他的选项则会在运行时被覆盖
类型 |
默认 |
|
---|---|---|
适配器会发送独立的HTTP调用到Keycloak服务器来把access code转换为access token。该选项定义了有多少到Keycloak服务器的连接应当被放入连接池 环境变量: |
int |
|
指定策略的执行模式. 环境变量: |
|
|
定义缓存数目限制 环境变量: |
int |
|
定义缓存项目过期时间(微秒ms) 环境变量: |
long |
|
指定适配器如何从服务器为您的应用获取路径相关的资源。如果为true,那么policy enforcer将会在路径被访问时按需获取对应资源。 环境变量: |
boolean |
|
指定范围如何被映射到HTTP方法上。如果为true,那么policy enforcer会使用当前HTTP请求的方法来检测该访问是否应当被授权 环境变量: |
boolean |
|
服务器中关联到给定路径的资源名称 环境变量: |
string |
|
指定需要受到policy enforcer保护的应用程序上下文路径相关的URI 环境变量: |
string |
|
指定HTTP方法名 环境变量: |
string |
required |
一个字符串数组,用以指定关联到HTTP方法的范围 环境变量: |
list of string |
required |
一个字符串,用以表示关联到HTTP方法的范围的执行模式 环境变量: |
|
|
指定策略执行的模式 环境变量: |
|
|
环境变量: |
|
|
环境变量: |
|
|
环境变量: |
|
|
Environment variable: |
|
|
类型 |
默认 |
|
适配器会发送独立的HTTP调用到Keycloak服务器来把access code转换为access token。该选项定义了有多少到Keycloak服务器的连接应当被放入连接池 环境变量: |
int |
|
指定策略执行模式 环境变量: |
|
|
服务器中关联到给定路径的资源名称 环境变量: |
string |
|
指定需要受到policy enforcer保护的应用程序上下文路径相关的URI 环境变量: |
string |
|
指定HTTP方法名 环境变量: |
string |
required |
一个字符串数组,用以指定关联到HTTP方法的范围 环境变量: |
list of string |
required |
一个字符串,用以表示关联到HTTP方法的范围的执行模式 环境变量: |
|
|
指定策略执行模式 环境变量: |
|
|
环境变量: |
|
|
环境变量: |
|
|
定义缓存数目限制 环境变量: |
int |
|
定义缓存项目过期时间(微秒ms) 环境变量: |
long |
|
指定适配器如何从服务器为您的应用获取路径相关的资源。如果为true,那么policy enforcer将会在路径被访问时按需获取对应资源 环境变量: |
boolean |
|
环境变量: |
|
|
环境变量: |
|
|
指定范围如何被映射到HTTP方法上。如果为true,那么policy enforcer会使用当前HTTP请求的方法来检测该访问是否应当被授权 环境变量: |
boolean |
|