Keycloak 策略执行器

在 Java 应用程序中使用 Keycloak 策略执行器

策略执行点 (PEP) 是一种设计模式,因此您可以通过不同的方式实现它。Keycloak 提供了所有必要的工具来为不同的平台、环境和编程语言实现 PEP。Keycloak 授权服务提供 RESTful API,并利用 OAuth2 授权功能,使用集中式授权服务器进行细粒度授权。

PEP overview

PEP 负责从 Keycloak 服务器执行访问决策,这些决策通过评估与受保护资源相关的策略来做出。它充当应用程序中的过滤器或拦截器,用于检查是否可以根据这些决策授予的权限,满足对受保护资源的特定请求。

Keycloak 为启用 **Keycloak 策略执行器** 到 Java 应用程序提供了内置支持,并具有内置支持来保护符合 JakartaEE 标准的框架和 Web 容器。如果您使用的是 Maven,则应将以下依赖项配置到您的项目中

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-policy-enforcer</artifactId>
    <version>26.0.2</version>
</dependency>

启用策略执行器后,发送到应用程序的所有请求都会被拦截,并且根据 Keycloak 授予执行请求的身份的权限,授予对受保护资源的访问权限。

策略执行与应用程序的路径和您使用 Keycloak 管理控制台为资源服务器创建的 资源 密切相关。默认情况下,当您创建资源服务器时,Keycloak 会为您的资源服务器创建一个 默认配置,以便您可以快速启用策略执行。

配置

策略执行器配置使用 JSON 格式,如果您想根据资源服务器提供的资源自动解析受保护的路径,大多数情况下您不需要设置任何内容。

如果您想手动定义要保护的资源,您可以使用更详细的格式

{
  "enforcement-mode" : "ENFORCING",
  "paths": [
    {
      "path" : "/users/*",
      "methods" : [
        {
          "method": "GET",
          "scopes" : ["urn:app.com:scopes:view"]
        },
        {
          "method": "POST",
          "scopes" : ["urn:app.com:scopes:create"]
        }
      ]
    }
  ]
}

以下是每个配置选项的描述

  • enforcement-mode

    指定如何执行策略。

    • ENFORCING

      (默认模式) 默认情况下拒绝请求,即使没有与给定资源关联的策略。

    • PERMISSIVE

      即使没有与给定资源关联的策略,也允许请求。

    • DISABLED

      完全禁用策略评估,并允许访问任何资源。当 enforcement-modeDISABLED 时,应用程序仍然可以通过 授权上下文 获取 Keycloak 授予的所有权限

  • on-deny-redirect-to

    定义一个 URL,当从服务器获取“拒绝访问”消息时,客户端请求将被重定向到该 URL。默认情况下,适配器会响应 403 HTTP 状态码。

  • path-cache

    定义策略执行器应如何跟踪应用程序中的路径与 Keycloak 中定义的资源之间的关联。缓存是必要的,以避免通过缓存路径和受保护资源之间的关联来避免不必要的 Keycloak 服务器请求。

    • lifespan

      定义条目应过期的毫秒数。如果未提供,默认值为 **30000**。可以设置等于 0 的值以完全禁用缓存。可以设置等于 -1 的值以禁用缓存的过期。

    • max-entries

      定义应保存在缓存中的条目限制。如果未提供,默认值为 **1000**。

  • paths

    指定要保护的路径。此配置是可选的。如果未定义,策略执行器将通过获取您在 Keycloak 中定义的应用程序的资源来发现所有路径,其中这些资源定义了表示应用程序中某些路径的 URIS

    • name

      服务器上要与给定路径关联的资源的名称。与 **path** 一起使用时,策略执行器将忽略资源的 **URIS** 属性,并使用您提供的路径。

    • path

      (必需) 相对于应用程序上下文路径的 URI。如果指定了此选项,策略执行器将查询服务器以获取具有相同值的 **URI** 的资源。目前支持非常基本的路径匹配逻辑。有效路径的示例如下:

      • 通配符: /*

      • 后缀: /*.html

      • 子路径: /path/*

      • 路径参数: /resource/{id}

      • 精确匹配: /resource

      • 模式: /{version}/resource, /api/{version}/resource, /api/{version}/resource/*

    • methods

      要保护的 HTTP 方法(例如 GET、POST、PATCH)以及它们如何与服务器中给定资源的范围关联。

      • method

        HTTP 方法的名称。

      • scopes

        一个包含与方法关联的范围的字符串数组。当您将范围与特定方法关联时,尝试访问受保护资源(或路径)的客户端必须提供一个 RPT,该 RPT 授予对列表中指定的所有范围的权限。例如,如果您定义了一个具有范围 create 的方法 POST,则当对路径执行 POST 时,RPT 必须包含一个授予对 create 范围的访问权限的权限。

      • scopes-enforcement-mode

        一个字符串,引用与方法关联的范围的执行模式。值可以是 **ALL** 或 **ANY**。如果为 **ALL**,则必须授予所有已定义的范围才能使用该方法访问资源。如果为 **ANY**,则必须授予至少一个范围才能使用该方法获得对资源的访问权限。默认情况下,执行模式设置为 **ALL**。

    • enforcement-mode

      指定如何执行策略。

      • ENFORCING

        (默认模式) 默认情况下拒绝请求,即使没有与给定资源关联的策略。

      • DISABLED

    • claim-information-point

      定义一组或多组必须解析并推送到 Keycloak 服务器的声明,以便使这些声明可供策略使用。有关详细信息,请参阅 声明信息点

  • lazy-load-paths

    指定适配器应如何获取服务器以获取与应用程序中的路径关联的资源。如果为 **true**,策略执行器将根据请求的路径按需获取资源。此配置在您不想在部署期间从服务器获取所有资源(如果您没有提供任何 paths)或如果您只定义了一部分 paths 并想按需获取其他资源的情况下特别有用。

  • http-method-as-scope

    指定应如何将范围映射到 HTTP 方法。如果设置为 **true**,策略执行器将使用当前请求中的 HTTP 方法来检查是否应授予访问权限。启用后,确保您的 Keycloak 中的资源与表示您要保护的每个 HTTP 方法的范围相关联。

  • claim-information-point

    定义一组或多组必须解析并推送到 Keycloak 服务器的 **全局** 声明,以便使这些声明可供策略使用。有关详细信息,请参阅 声明信息点

声明信息点

声明信息点 (CIP) 负责解析声明并将这些声明推送到 Keycloak 服务器,以便为策略提供有关访问上下文的更多信息。它们可以作为策略执行器的配置选项来定义,以便从不同来源解析声明,例如

  • HTTP 请求(参数、标头、主体等)

  • 外部 HTTP 服务

  • 在配置中定义的静态值

  • 通过实现声明信息提供者 SPI 的任何其他来源

将声明推送到 Keycloak 服务器时,策略不仅可以根据用户的身份做出决策,还可以根据给定事务的 who、what、why、when、where 和 which,将上下文和内容考虑在内。它与基于上下文的授权以及如何在运行时使用信息来支持细粒度授权决策有关。

从 HTTP 请求获取信息

以下是一些示例,展示如何从 HTTP 请求中提取声明

keycloak.json
{
  "paths": [
    {
      "path": "/protected/resource",
      "claim-information-point": {
        "claims": {
          "claim-from-request-parameter": "{request.parameter['a']}",
          "claim-from-header": "{request.header['b']}",
          "claim-from-cookie": "{request.cookie['c']}",
          "claim-from-remoteAddr": "{request.remoteAddr}",
          "claim-from-method": "{request.method}",
          "claim-from-uri": "{request.uri}",
          "claim-from-relativePath": "{request.relativePath}",
          "claim-from-secure": "{request.secure}",
          "claim-from-json-body-object": "{request.body['/a/b/c']}",
          "claim-from-json-body-array": "{request.body['/d/1']}",
          "claim-from-body": "{request.body}",
          "claim-from-static-value": "static value",
          "claim-from-multiple-static-value": ["static", "value"],
          "param-replace-multiple-placeholder": "Test {keycloak.access_token['/custom_claim/0']} and {request.parameter['a']}"
        }
      }
    }
  ]
}

从外部 HTTP 服务获取信息

以下是一些示例,展示如何从外部 HTTP 服务中提取声明

keycloak.json
{
  "paths": [
    {
      "path": "/protected/resource",
      "claim-information-point": {
        "http": {
          "claims": {
            "claim-a": "/a",
            "claim-d": "/d",
            "claim-d0": "/d/0",
            "claim-d-all": [
              "/d/0",
              "/d/1"
            ]
          },
          "url": "http://mycompany/claim-provider",
          "method": "POST",
          "headers": {
            "Content-Type": "application/x-www-form-urlencoded",
            "header-b": [
              "header-b-value1",
              "header-b-value2"
            ],
            "Authorization": "Bearer {keycloak.access_token}"
          },
          "parameters": {
            "param-a": [
              "param-a-value1",
              "param-a-value2"
            ],
            "param-subject": "{keycloak.access_token['/sub']}",
            "param-user-name": "{keycloak.access_token['/preferred_username']}",
            "param-other-claims": "{keycloak.access_token['/custom_claim']}"
          }
        }
      }
    }
  ]
}

静态声明

keycloak.json
{
  "paths": [
    {
      "path": "/protected/resource",
      "claim-information-point": {
        "claims": {
          "claim-from-static-value": "static value",
          "claim-from-multiple-static-value": ["static", "value"]
        }
      }
    }
  ]
}

声明信息提供者 SPI

如果内置提供者都不足以满足开发人员的要求,声明信息提供者 SPI 可用于支持不同的声明信息点。

例如,要实现新的 CIP 提供者,您需要实现 org.keycloak.adapters.authorization.ClaimInformationPointProviderFactoryClaimInformationPointProvider,并在应用程序的类路径中提供文件 META-INF/services/org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory

org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory 的示例

public class MyClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory<MyClaimInformationPointProvider> {

    @Override
    public String getName() {
        return "my-claims";
    }

    @Override
    public void init(PolicyEnforcer policyEnforcer) {

    }

    @Override
    public MyClaimInformationPointProvider create(Map<String, Object> config) {
        return new MyClaimInformationPointProvider(config);
    }
}

每个 CIP 提供者都必须与一个名称关联,如上面在 MyClaimInformationPointProviderFactory.getName 方法中定义的那样。此名称将用于将 policy-enforcer 配置中 claim-information-point 部分的配置映射到实现。

处理请求时,策略执行器将调用 MyClaimInformationPointProviderFactory.create 方法以获取 MyClaimInformationPointProvider 的实例。调用时,将任何为此特定 CIP 提供者(通过 claim-information-point)定义的配置作为映射传递。

ClaimInformationPointProvider 的示例

public class MyClaimInformationPointProvider implements ClaimInformationPointProvider {

    private final Map<String, Object> config;

    public MyClaimInformationPointProvider(Map<String, Object> config) {
        this.config = config;
    }

    @Override
    public Map<String, List<String>> resolve(HttpFacade httpFacade) {
        Map<String, List<String>> claims = new HashMap<>();

        // put whatever claim you want into the map

        return claims;
    }
}

获取授权上下文

启用策略执行后,从服务器获取的权限可通过 org.keycloak.AuthorizationContext 获得。此类提供了多种方法,您可以使用这些方法来获取权限并确定是否已为特定资源或范围授予权限。

在 Servlet 容器中获取授权上下文

HttpServletRequest request = // obtain javax.servlet.http.HttpServletRequest
AuthorizationContext authzContext = (AuthorizationContext) request.getAttribute(AuthorizationContext.class.getName());
授权上下文有助于您更好地控制服务器做出的决策。例如,您可以使用它来构建动态菜单,其中项目根据与资源或范围关联的权限隐藏或显示。
if (authzContext.hasResourcePermission("Project Resource")) {
    // user can access the Project Resource
}

if (authzContext.hasResourcePermission("Admin Resource")) {
    // user can access administration resources
}

if (authzContext.hasScopePermission("urn:project.com:project:create")) {
    // user can create new projects
}

AuthorizationContext 代表了 Keycloak 授权服务的主要功能之一。从上面的示例可以看出,受保护资源并未直接与管理它们的策略相关联。

考虑一些使用基于角色的访问控制 (RBAC) 的类似代码

if (User.hasRole('user')) {
    // user can access the Project Resource
}

if (User.hasRole('admin')) {
    // user can access administration resources
}

if (User.hasRole('project-manager')) {
    // user can create new projects
}

尽管这两个示例都解决了相同的要求,但它们使用的是不同的方法。在 RBAC 中,角色仅 *隐式* 定义对资源的访问权限。使用 Keycloak,您可以获得创建更易于管理的代码的能力,该代码直接关注您的资源,无论您是使用 RBAC、基于属性的访问控制 (ABAC) 还是任何其他 BAC 变体。您要么拥有对给定资源或范围的权限,要么没有该权限。

现在,假设您的安全要求已更改,除了项目经理,PMO 也能创建新项目。

安全要求会发生变化,但使用 Keycloak,您无需更改应用程序代码来满足新的要求。一旦您的应用程序基于资源和范围标识符,您只需要更改授权服务器中与特定资源关联的权限或策略的配置即可。在这种情况下,将更改与 项目资源 和/或范围 urn:project.com:project:create 关联的权限和策略。

使用 AuthorizationContext 获取授权客户端实例

AuthorizationContext 也可用于获取对配置到应用程序的 Keycloak 授权客户端 的引用

ClientAuthorizationContext clientContext = ClientAuthorizationContext.class.cast(authzContext);
AuthzClient authzClient = clientContext.getClient();

在某些情况下,由策略执行器保护的资源服务器需要访问授权服务器提供的 API。有了 AuthzClient 实例,资源服务器可以与服务器交互以创建资源或以编程方式检查特定权限。

配置 TLS/HTTPS

当服务器使用 HTTPS 时,请确保您的策略执行器按如下方式配置

{
  "truststore": "path_to_your_trust_store",
  "truststore-password": "trust_store_password"
}

以上配置启用授权客户端的 TLS/HTTPS,使您可以使用 HTTPS 方案远程访问 Keycloak 服务器。

强烈建议您在访问 Keycloak 服务器端点时启用 TLS/HTTPS。
在本页