
策略执行点 (PEP) 是一种设计模式,因此您可以用不同的方式实现它。Keycloak 提供了在不同平台、环境和编程语言中实现 PEP 的所有必要手段。Keycloak 授权服务提供了一个 RESTful API,并利用 OAuth2 授权功能,通过集中式授权服务器进行细粒度的授权。
PEP 负责执行来自 Keycloak 服务器的访问决策,这些决策是通过评估与受保护资源关联的策略来制定的。它在您的应用程序中充当过滤器或拦截器,以便根据这些决策授予的权限来检查是否可以满足对受保护资源的特定请求。
Keycloak 为 Java 应用程序提供内置支持以启用 Keycloak 策略执行器,并内置支持以保护符合 JakartaEE 标准的框架和 Web 容器。如果您使用 Maven,则应将以下依赖项配置到您的项目中
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
<version>26.0.4</version>
</dependency>
当您启用策略执行器后,发送到您应用程序的所有请求都将被拦截,并且对受保护资源的访问将取决于 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-mode
为 DISABLED
时,应用程序仍然能够通过 授权上下文 获取 Keycloak 授予的所有权限
on-deny-redirect-to
定义从服务器收到“访问被拒绝”消息时,客户端请求重定向到的 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 授予对列表中指定的所有范围的权限。例如,如果您定义一个方法 POST,其范围为 create,则当对路径执行 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 服务器时,策略不仅可以根据用户是谁来制定决策,还可以根据给定事务的谁、什么、为什么、何时、何地和哪个来考虑上下文和内容。这一切都与基于上下文的授权以及如何使用运行时信息来支持细粒度的授权决策有关。
以下是一些示例,说明如何从 HTTP 请求中提取声明
{
"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 服务中提取声明
{
"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']}"
}
}
}
}
]
}
{
"paths": [
{
"path": "/protected/resource",
"claim-information-point": {
"claims": {
"claim-from-static-value": "static value",
"claim-from-multiple-static-value": ["static", "value"]
}
}
}
]
}
开发人员可以使用声明信息提供程序 SPI 来支持不同的声明信息点,以防内置提供程序都不能满足他们的需求。
例如,要实现新的 CIP 提供程序,您需要实现 org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory
和 ClaimInformationPointProvider
,并在应用程序的类路径中提供文件 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,无需更改您的应用程序代码来满足新的要求。一旦您的应用程序基于资源和范围标识符,您只需更改授权服务器中与特定资源关联的权限或策略的配置即可。在这种情况下,与 Project Resource
和/或范围 urn:project.com:project:create
关联的权限和策略将被更改。
也可以用于获取对配置到您的应用程序的 Keycloak 授权客户端 的引用AuthorizationContext
ClientAuthorizationContext clientContext = ClientAuthorizationContext.class.cast(authzContext);
AuthzClient authzClient = clientContext.getClient();
在某些情况下,受策略执行器保护的资源服务器需要访问授权服务器提供的 API。有了 AuthzClient
实例,资源服务器可以与服务器交互,以便以编程方式创建资源或检查特定权限。