Keycloak授权服务指南

2023-05-16

为什么80%的码农都做不了架构师?>>>   hot3.png

本文译自Keycloak官方文档,原文链接。对应版本为5.0。

概述

Keycloak支持细粒度的授权策略,并可以对这些策略进一步组合,如:

  • 基于属性(ABAC)
  • 基于角色(RBAC)
  • 基于用户(UBAC)
  • 基于上下文(CABC)
  • 基于规则(Rule-based)
    • 通过JavaScript
    • 使用Jboss Drools
  • 基于时间(Time-based)
  • 通过SPI自定义访问控制策略(ACM)

Keycloak提供了一组管理界面和RESTful API,用于创建权限、关联权限与授权策略,以及在应用程序中执行授权决策。

资源服务器需要依据一些信息才能判断权限。对于REST的资源服务器来说这些信息通常来自于一个加密的token,如在每个访问请求中携带bearer token。对于依赖于session的Web应用来说,这些信息就来自于每个request对应的用户session。

资源服务器通常执行的是基于角色的访问控制(RBAC)策略,即检查用户所拥有的角色是否关联了要访问的资源。虽然这种方式非常有用,但是它们也有一些限制:

  • 资源和角色紧耦合,角色的更改(如添加、删除或更改访问上下文)可能影响多个资源。
  • 基于RBAC的应用程序无法很好地响应安全性需求的调整。
  • 项目规模扩大时,复杂的角色管理会很困难而且容易出错。
  • 不够灵活。角色并不能有效代表用户身份,即缺乏上下文信息。客观上来说被授予了角色的用户,至少会拥有某些访问权限。

当下的项目中,我们需要考虑不同地区、不同本地策略、使用不同设备、以及对信息共享有较高需求的异构环境。Keycloak授权服务可以通过以下方式帮助您提高应用程序和服务的授权能力:

  • 不同的访问控制机制以及细粒度的授权策略。
  • 中心化的资源、权限以及策略管理。
  • 中心化的策略决策。
  • REST风格的授权服务。
  • 授权工作流以及用户访问管理。
  • 可作为快速响应您项目中安全需求的基础设施。

架构

从设计的角度来看,授权服务基于一组授权模式,提供了以下功能:

  • 策略管理点(PAP)

在Keycloak Admin的基础上提供了UI界面来管理资源服务器、资源、范围、权限以及策略。其中的部分功能也可以通过Protection API来实现。

  • 策略决策点(PDP)

为和权限请求相应的授权请求以及策略评估,提供了分布式的策略决策点。更多信息请查看获取权限章节。

  • 策略执行点(PEP)

为资源服务器端实际执行授权决策,Keycloak提供了用于不同环境的一些内置策略执行器。

  • 策略信息点(PIP)

基于Keycloak的认证服务器,可以在策略评估时从身份认证信息以及上下文环境中获取一些其它属性。

授权过程

Keycloak的细粒度授权主要有三个必要步骤:

  • 资源管理
  • 权限及策略管理
  • 执行策略

资源管理

资源管理定义了什么是被保护的对象。

首先,需要定义被保护的资源服务器,通常是一个web应用或一组服务。有关资源服务器的更多信息请参考术语。

资源服务器可以使用Keycloak管理员控制台来管理。在那里,您可以将任何已注册的客户端启用为资源服务器,并管理其资源和范围。

资源可以是web页面、Rest资源、文件系统上的一个文件、一个EJB等等。它们可以是一组资源(如Java中的一个Class),也可以是一个特定的资源。

举例来说,你可以用Bank Account来代表所有的银行账户,并且用它来定义对全部银行账户的通用授权。但同时你也可以为Alice的私有账户来单独设置权限,使得只有Alice本人能够对其账户进行某些操作。

资源可以通过Keycloak控制台或者Protection API来管理。利用Protection API的方式,资源服务器可以远程管理资源。

范围(Scope)通常用来表示在资源上执行的动作,还可以使用范围来代表资源中的一个或者多个属性。

权限及策略管理

定义好了资源服务器和其上的资源,下一步就是定义权限和策略。

定义策略的步骤图示如下:

策略定义了在资源或者范围上执行某些动作的先决条件,但是请记住它们并不和被保护的资源直接绑定。策略是通用的,可以通过再次组合来构造更复杂的权限及策略。

举例来说,要获取“User Premium”角色下的资源组访问权限,你可以使用RBAC策略。

Keycloak为常用场景提供了一些内置的策略类型。你可以使用JavaScript或者JBoss的Drools来编写自己的策略。

定义了策略之后就可以来定义权限了。权限与被保护的资源紧耦合,它们由被保护对象及策略组合而成。

执行策略

策略的执行包含了对资源服务器实际实施授权决策的必要步骤。实现方式是在资源服务器上启用策略执行点(PEP),PEP能够与授权服务器通信,请求授权数据,并根据服务器返回的决策来控制对受保护资源的访问。

Keycloak提供了一些内置的PEP实现,你可以根据项目运行平台来自由选择。

授权服务

授权服务包括下列三种REST端点:

  • Token Endpoint
  • Resource Management Endpoint
  • Permission Managerment Endpoint

上边的各个端点涵盖了授权服务的各个步骤。

Token Endpoint

Oauth2客户端(如前端应用)可以通过token endpoint获取访问令牌(access token),然后使用这些令牌来获取资源服务器(如后端应用)上被保护的资源。同样,Keycloak授权服务扩展了OAuth2,允许基于配置好的策略发放访问令牌。这意味着资源服务器可以利用关联了权限的令牌来对资源进行保护。在Keycloak中,带有权限的访问令牌被称作请求方令牌(Requesting Party Token),或者缩写为RPT。

更多的信息请查看获取权限章节。

保护API

保护API指提供的一系列UMA兼容的操作,用来帮助资源服务器管理资源、范围、权限及相关策略。只有资源服务器才能被允许访问这些API,前提是资源服务器有内置的uma_protection范围。

提供给资源服务器的API可以分为两组:

  • 资源管理

创建资源、删除资源、根据ID查询、其他查询

  • 权限管理

发放权限Ticket

远程管理资源的功能默认是开启的。可以通过Keycloak管理员控制台来关闭此功能。

如果使用了UMA协议,Protection API对权限Ticket的发放是整个授权流程的重要组成部分。后面会讲到,它们表示着在整个授权过程中客户端所申请的权限及向服务器申请包含着权限的最终令牌。

更多信息请查看保护API章节。

名词解释

在深入到Keycloak前我们需要解释一下Keycloak授权服务中所用到的专有名词。

资源服务器(Resource Server)

如前所述,资源服务器通常依赖某种信息来决定是否授权。该授权信息通常包含在安全令牌或用户会话中。

任何可信的Keycloak客户端都可以作为资源服务器。这些客户端的资源及范围由一系列的授权策略保护。

资源

资源是应用或者组织的资产。它们可以是一些列的端点、一个典型的HTML页面等等。在授权策略语境中,资源指的就是被保护的对象。

每个资源或者每组资源都有着唯一标识。请回想上文的Bank例子。

范围

资源的范围扩展了资源的访问界限。在授权语义中,范围是可以应用在资源上的许多动词之一。

它通常表示对指定资源可施加的动作。如查看、编辑、删除等等。但是范围也可以表示资源的其它关联信息。如工程资源和造价范围,这里的造价范围被用来定义用户访问工程造价的特定策略及权限。

权限

权限将被保护的对象与必须评估的策略关联起来,以确定是否允许访问。

  • X 可以在资源 Z 上施加 Y

这里X指可以是用户、角色、用户组,或者其组合。在这里你可以使用声明和上下文。

Y指代一个动作,如写、读等等。

Z指代被保护的资源,如 "/accounts"。

Keycloak提供了丰富的平台用于构建从简单到复杂的权限策略,如基于规则的权限控制。使用Keycloak可以带来:

  • 减少代码重构和权限管理成本。
  • 支持灵活的安全模型,用以应对安全模型可能的变化。
  • 支持运行时变化。应用系统只需要关心资源和范围,Keycloak隐藏了它们如何被保护的细节

策略

策略定义了授予对象访问权时必须满足的条件。与权限不同,策略不指定受保护的对象,而是指定访问制定对象(如资源、范围或两者)时必须满足的条件。策略与用来保护资源的访问控制机制(ACMs)密切相关。使用策略可以实现基于属性的访问控制(ABAC)、基于角色的访问控制(RBAC)、基于上下文的访问控制以及其任何组合。

Keycloak还提供了聚合策略,即“策略的策略”。在构造复杂的访问控制条件时,Keycloak授权服务遵循了分而治之的原则。你可以创建独立的策略,并在不同的权限中使用它们,然后通过组合它们来构造更复杂的策略。

策略提供者(Policy Provider)

策略提供者是特定策略类型的具体实现。Keycloak提供的内置策略都是由不同的提供者来支持的。Keycloak允许添加自定义的策略类型。

Keyclaok提供了SPI(Service Provider Interface),你可以使用SPI来添加自定义策略提供者。

权限许可(Permission Ticket)

权限许可(权限ticket)是由UMA定义的特殊token类型,它的不透明结构由授权服务器所决定。这种结构代表了客户端请求的资源或者范围、访问上下文,以及要执行的策略。

在UMA中,权限许可是人与人、人与组织之间共享的关键。在授权工作流中使用权限ticket可以支持一系列从简单到复杂的场景,让资源所有者和资源服务器细粒度地控制资源访问。

在UMA工作流中,授权服务器向资源服务器发出权限ticket,资源服务器将ticket返回给试图访问受保护资源的客户端。客户端拿到ticket就可以通过将它发送回授权服务器来请求RPT(最终的包含授权数据的令牌)。

更多授权许可的信息请查看UMA章节及UMA规范。

开始

在使用本教程之前,请确保正确安装了Keycloak并已经初始化了管理员账户。注意,你必须在Keycloak同一台主机上运行独立的WildFly实例。这个独立的实例将用来运行你的Java Servlet应用。你可以在启动命令中使用jboss.socket.binding.port-offset来避免端口冲突。

可以用如下命令来启动Keycloak服务(Linux版):

$ .../bin/standalone.sh -Djboss.socket.binding.port-offset=100

Windows版:

> ...\bin\standalone.bat -Djboss.socket.binding.port-offset=100

更多安装配置WildFly的细节请查看这里。

正确安装启动之后,可以从 http://localhost:8180/auth/admin 访问Keycloak控制台,以及从 http://localhost:8080 访问WildFly实例。

保护Servlet应用

本入门指南旨在让您尽快启动、运行及测试Keycloak提供的各种授权特性。它主要依赖于默认的数据库和服务器配置,并不涉及复杂的部署选项。有关特性或配置选项的更多信息,请参阅本文档中的相关部分。

本指南阐述了Keycloak授权服务的关键概念:

  • 为客户应用提供细粒度的授权
  • 将客户应用设置为资源服务器并保护其资源
  • 定义权限及授权策略来保护资源
  • 在应用中启用PEP

创建域(Realm)及用户

首先来创建域及域中的用户。然后在域中创建客户应用,即需要保护的资源服务器。

依照以下步骤来创建域及用户:

1. 创建名为 hello-world-authz 的域。创建成功之后的页面如下图:

2. 创建用户。点击 Users,打开用户列表页。

3. 在右侧空白的用户列表页点击 Add User

4. 填写Username、Email、First Name及Last Name,打开User Enabled选项,然后点击 Save。

5. 在Credentials选项卡修改用户密码。

6. 填写New Password、及Password Confirmation,然后关闭Temporary

7. 点击Reset Password完成设置用户密码。

启用授权服务

你可以为已有的客户端应用开启OpenID Connect协议。也可以创建一个新的客户端。

依照以下步骤来创建新客户端:

1. 点击Clients来创建一个新的应用,并填写Client IDClient Protocol,以及Root URL

2. 点击Save

3. 开启Authorization Enabled开关,会打开新的Authorization选项卡。

4. 点击Authorization选项卡:

一旦为客户端应用开启了授权服务,Keycloak会自动创建一些授权默认配置。

有关授权配置的更多信息,请查看开启服务授权章节。

构建、部署及测试

到这里,app-authz-vanilla资源服务器应该已经被正确配置完毕了,现在我们开始部署。项目可以在这里直接下载。部署及构建的前置要求如下:

  • Java JDK 8
  • Apache Maven 3.1.1或更高版本
  • Git

项目基于最新的Keycloak版本。可以使用如下命令来克隆代码:

$ git clone https://github.com/keycloak/keycloak-quickstarts

要构建的项目地址在:

$ cd keycloak-quickstarts/app-authz-jee-vanilla

获取适配器配置

构建项目之前请依照以下步骤来获取适配器配置。

1. 点击Clients。在client列表中,点击app-authz-vanilla

2. 点击 Installation 选项卡。在下拉列表中选中Keycloak OIDC JSON格式。然后点击Download

3. 将keycloak.json移到app-authz-jee-vanilla/config目录。

4. 默认情况下,访问无权限的资源时Policy执行器会返回403。你也可以定义一个重定向页面。要定义重定向页面需要修改第3步中keycloak.json文件的policy-enforcer配置:

"policy-enforcer": {
    "on-deny-redirect-to" : "/app-authz-vanilla/error.jsp"
}

上面的配置将403重定向到一个错误页面。

构建及部署应用

执行下面的命令来构建、部署应用:

$ cd redhat-sso-quickstarts/app-authz-jee-vanilla
$ mvn clean package wildfly:deploy

测试应用

如果成功部署了应用,你可以从 http://localhost:800/app-authz-vanilla 来访问Keycloak登录页。

使用刚创建的alice用户登录。

在为客户端启用授权服务时,Keycloak默认提供了一个简单的策略,该策略授予所有保护的资源以访问权限。

现在你可修改默认的权限及策略,然后来测试程序的响应,或者使用不同的策略类型来创建新的策略。

可以点击Authorization选项卡中的Policies选项卡,然后点击Default Policy来修改:

// The default value is $evaluation.grant(),
// let's see what happens when we change it to $evaluation.deny()
$evaluation.deny();

现在,退出demo应用并重新登录。你会发现无法访问它了:

现在我们不更改默认的策略代码,而是将策略代码文本区域下方的Logic更改为Negative。这将重新启用对应用程序的访问,默认情况下,该策略拒绝所有访问请求。同样,在测试此更改之前请记得注销并重新登录。

下一步

我们还可以:

  • 创建一个范围,定义一个策略及它的权限,然后在应用端来进行测试。看用户是否能够执行动作(或者你刚刚创建的范围)?
  • 创建不同类型的策略,如基于规则的,并将这些策略和默认权限关联起来。
  • 对默认权限应用多个策略并测试。例如,组合多个策略并相应地更改决策策略(Decision Strategy)。
  • 关于查看及测试权限的更多信息,可以查看获取授权上下文章节。

授权快速入门

除了app-authz-jee-vanilla快速开始指南,Keycloak Quickstarts仓库中也包含本文档中的其他例子。

授权快速开始指南只是一个起点,用于帮助你快速浏览在各种场景下keycloak提供的不同技术以及集成方式。它并不是一个全面的用户手册。

每个快速开始的项目都有一个README文档来介绍如何构建、部署及测试。下表是一个简单的介绍:

NameDescription
app-authz-jee-servlet介绍在Java EE项目中如何精细化管理权限,并基于从Keycloak服务器获取到的权限来动态构建菜单
app-authz-jee-vanilla介绍在Java EE项目中如何精细化管理权限,并使用默认的授权设置来保护资源
app-authz-rest-springboot介绍如何使用Keycloak授权服务来保护SpringBoot项目
app-authz-springboot介绍如何用切面来认证并授权SpringBoot REST服务
app-authz-uma-photoz这是一个简单的基于HTML5+AngularJS+JAX-RS的项目,用来展示如何使用UMA来让用户管理自己的权限

管理资源服务器

根据OAuth2规范,资源服务器托管资源,并接受和响应对受保护资源的请求。

Keycloak为资源服务器提供了一个丰富的平台,用于为受保护的资源启用细粒度授权,并可以根据不同的访问控制机制来决策授权。

可以为任意客户端开启细粒度授权。这样做的时候你其实已经将一个客户端当做了一个资源服务器。

创建客户端应用程序

要开启Keycloak授权服务,必须先创建一个资源服务器。

1. 点击Clients

2. 在上面的页面中点击 Create

3. 输入Client ID。如my-resource-server。

4. 输入Root URL。如:

http://${host}:${port}/my-resource-server

5. 点击 Save。然后开始配置client设置:

开启授权服务

Authorization Enabled开关设置为ON并保存,就可以将OIDC 客户端设为一个资源服务器并开启细粒度授权。

上面的操作会打开新的Authorization选项卡:

Authorization选项卡下方会带有子选项卡。我们后边会讲解每个子选项卡的作用。先来快速浏览一下主要功能:

  • Settings

包含资源服务器的常规设置。更多细节请查看这里。

  • Resource

在这里可以管理客户端应用的资源。

  • Authorization Scopes

在这里管理范围。

  • Policies

在这里管理策略,以及定义授权时必须满足的条件。

  • Permissions

在这里可以通过将资源/范围和策略关联起来来设置它们的权限。

  • Evaluate

在这里模拟授权场景,并观察结果是否和预期一致。

  • Export Settings

在这里可以导出授权配置到JSON文件。

资源服务器设置

在资源服务器设置页面可以设置策略执行模式,是否允许远程资源管理,并可以导出授权配置。

  • 策略执行模式
    • 强制 默认模式,对没有关联策略的资源的访问会直接被拒绝。
    • 自由 对没有关联到策略的资源的访问会被允许。
    • 禁用 禁用所有策略的评估,并直接允许访问。
  • 远程资源管理

定义资源是否可被远程管理,关闭后资源仅能在管理员控制台管理。

默认配置

创建好了资源服务器,Keycloak会为其自动创建默认配置。

默认配置包括了:

  • 一个默认的受保护资源(通配符),它代表着项目中全部资源。
  • 一个默认策略,始终授予对受此策略保护的资源的访问权(即默认全部授权)。
  • 一个默认权限,它基于默认策略来管理对所有资源的访问的权限。

这个默认的受保护资源被称为默认资源,导航到Resources选项卡就可以看到它:

这个资源定义了一个名为 urn:my-resource-server:resources:default 的类型,以及一个 /* 的URI。这里的URI以通配符的形式匹配着项目中的全部路径。换句话说,当为应用程序启用策略强制时,将在授予访问权之前检查与资源关联的所有权限。

上面所说的类型可以用来被用来创建基于类型的资源权限,来管理属于同一类型的资源。

现在可以在Policies选项卡界面查看默认的realm策略。

此策略是一个基于JavaScript的策略,此策略定义了永远无条件授权。如果点击了策略你可以看到它定义的规则如下:

// by default, grants any permission associated with this policy
$evaluation.grant();

最后来看默认权限,如果导航到Permissions选项卡,就可以查看它:

此权限是一个基于资源的权限,定义了一组策略(一个或多个)应用于具有给定类型的所有资源。

修改默认配置

可以通过先删除后新建的方式来修改默认配置。

注意默认的资源以通配符的形式匹配着所有路径,在新建时不要产生冲突。

默认配置定义了一个映射到应用程序中的所有路径的资源。如果要对自己的资源写入权限,请确保删除默认资源或将其URI字段更改为应用程序中更特定的路径。否则,与默认资源关联的策略默认情况下总是授予访问权。

导入导出授权配置

资源服务器的配置可以自由导入导出。当你希望为资源服务器创建初始配置或更新现有配置时这项功能非常有用。导出的配置包含:

  • 被保护的资源和范围
  • 策略
  • 权限

导出

遵循以下步骤导出配置:

1. 进入Resource Server Settings 页面。

2. 点击Export Settings选项卡。

3. 点击Export按钮。

导出的文件是JSON格式,导出前会在text area中展示,你也可以选择从这里复制粘贴它。

导入配置文件

依照以下步骤来导入配置文件。

1. 导航到Resource Server Settings页面。

通过Select file选择要导入的配置然后加载。

管理资源和范围

资源管理功能相当直观。只要创建了资源服务器,就可以创建要保护的资源和范围。可以从ResourceScope选项卡来管理它们。

查看资源

Resouce页面,你可以查看当前资源服务器上的资源列表。

资源列表展示了被保护的资源的:

  • 类型
  • URIS
  • 所有者
  • 关联的范围(如果存在)
  • 关联的权限

你可以从列表中选择资源点击Create Permission来为其创建权限。

创建权限前请确保已定义好需要关联的策略。

创建资源

主要需要考虑的是资源的粒度。Keycloak的Resource概念可以用来表示一组(一个或多个)资源,如何定义它们对于管理权限非常重要。

要创建新的资源,请点击资源列表页面的右上角Create按钮。

在Keycloak中,资源定义了一组通用的信息,如:

  • Name

可读的、全局唯一的字符串

  • Type

唯一标识一组资源类型的字符串,用来对不同资源实例分组。如自动创建的默认资源的默认类型是 urn:resource-server-name:resources:default

  • URIS

代表着资源的地址。对于HTTP资源来说,URIS通常是资源的相对路径。

  • 范围

资源关联的一个或多个范围。

资源属性

资源可能具有与其相关联的属性。属性可在评估权限时用于向策略提供附加信息。

每个属性都是一个键和值对,值可以是字符串集合。属性的多个值可以用逗号加以区分。

分类资源

资源的type字段可用于将不同的资源归类,从而可以使用一组公共权限来保护它们。

资源所有者

资源都有一个拥有者。默认情况下资源属于资源服务器。

但资源也可以属于用户,从而让你创建用户相关的策略。如仅有资源拥有者可以更新或者删除指定的资源。

资源的远程管理

资源远程管理接口通过Protection API暴露。

当使用Protection API时,可以让资源服务器管理来用户的资源。此时可以指定用户ID来将资源设置为属于特定用户。

Keycloak支持资源服务器对其资源的完全控制。将来Keycloak计划允许用户完全控制自己的资源,以及批准授权请求和管理权限,特别是使用UMA协议时。

管理策略

如前所属,策略用来定义授权前需要满足的特定条件。

可以浏览Policy选项卡来查看当前资源服务器上的所有策略:

你可以在这里创建及编辑策略。

要创建新的策略,请从右上角的Create policy下拉列表中选择一个策略类型。接下来我们将详细描述每种策略类型。

基于用户的策略

基于用户的策略可以限制只有指定的用户能够访问被保护的资源。

要创建新的基于用户的策略,请在新建策略时选中User选项。

配置

  • 名称

名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

此策略的详情

  • Users

指定哪些用户可以被此策略授权。

  • 逻辑

在评估了此策略相关条件之后再应用的逻辑。

基于角色的策略

你可以使用这种类型的策略来定义允许一组角色访问对象的条件。

默认情况下,如果发起请求的用户已被授予此策略中任意一个角色就可以获得授权。也可以根据需要指定特定的必要角色,没有必要角色的用户将不会获得授权。可以自由组合必需角色和非必需角色,包括领域角色以及客户端角色。

要创建基于角色的策略,请在新建策略时选中Role

配置

  • 名称

名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

此策略的详情

  • Realms Roles

指定哪些realm角色被此策略授权。

  • Client Roles

指定哪些client角色被此策略授权。要启用此功能需要首先选择一个Client。

  • 逻辑

在评估了此策略相关条件之后再应用的逻辑。

定义必要角色

如前所述可以将策略中的角色指定为必要角色。仅当用户被授予了所有必要角色时,才会被授权。必要角色可以是realm角色,也可以是client角色。

若要按需指定角色,请选中角色后面的Required复选框。

必需的角色的意义在于:你的策略定义了多个角色,但仅某个子集是强制性的。在这种情况下,你可以通过组合realm和client角色来启用更细粒度的RBAC。例如可以为客户端订制特定的策略来要求指定的客户端角色。或者仅在特定realm角色存在时才授予访问权限。你可以在同一个策略中组合这两种方法。

基于JavaScript的策略

这种策略允许你编写JavaScript脚本来定义条件。它是Keycloak支持的基于规则的策略类型(rule-based)之一,它基于Evaluate API几乎可以提供任意的灵活性。

要创建基于JS的策略,请在创建策略时选中JavaScript

配置

  • 名称

名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

此策略的详情

  • Code

此策略的JavaScript脚本。

  • 逻辑

在评估完此策略相关条件之后再应用的逻辑。

例子

检查来自评估上下文的属性

这是一个基于JS来实现基于属性的权限控制(ABAC)的例子,其中的属性从执行上下文环境中获取:

var context = $evaluation.getContext();
var contextAttributes = context.getAttributes();

if (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {
    $evaluation.grant();
}

检查请求方身份的属性

下面基于ABAC的小例子展示了如何检查申请方的身份:

var context = $evaluation.getContext();
var identity = context.getIdentity();
var attributes = identity.getAttributes();
var email = attributes.getValue('email').asString(0);

if (email.endsWith('@keycloak.org')) {
    $evaluation.grant();
}

代码中所用到的属性映射自授权请求携带的token中预先好定义的声明。

检查请求方的角色

下面的代码展示如何检查用户是否被授予了keycloak_user的域角色

var context = $evaluation.getContext();
var identity = context.getIdentity();

if (identity.hasRealmRole('keycloak_user')) {
    $evaluation.grant();
}

也可以检查用户是否被授予了my-client-role 的客户端角色,其中my-client是客户机应用程序的客户机id

var context = $evaluation.getContext();
var identity = context.getIdentity();

if (identity.hasClientRole('my-client', 'my-client-role')) {
    $evaluation.grant();
}

检查角色是否授予了一个用户

下面的代码检查指定的域角色是否授予了一个用户:

var realm = $evaluation.getRealm();

if (realm.isUserInRealmRole('marta', 'role-a')) {
    $evaluation.grant();
}

下面检查指定的客户端角色是否授予了一个用户:

var realm = $evaluation.getRealm();

if (realm.isUserInClientRole('marta', 'my-client', 'some-client-role')) {
    $evaluation.grant();
}

检查角色是否授予了一个组

var realm = $evaluation.getRealm();

if (realm.isGroupInRole('/Group A/Group D', 'role-a')) {
    $evaluation.grant();
}

推送任意声明到服务器

可以向资源服务器推送任意声明,以便提供应如何执行权限的附加信息:

var permission = $evaluation.getPermission();

// decide if permission should be granted

if (granted) {
    permission.addClaim('claim-a', 'claim-a');
    permission.addClaim('claim-a', 'claim-a1');
    permission.addClaim('claim-b', 'claim-b');
}

检查用户是否属于某个组

var realm = $evaluation.getRealm();

if (realm.isUserInGroup('marta', '/Group A/Group B')) {
    $evaluation.grant();
}

使用多重访问控制机制

你可以自由组合多种访问控制机制。下面的例子展示了在同一个策略中组合RBAC和ABAC,它将检查user是否有admin角色,并且其邮箱是否属于keycloak.org域:

var context = $evaluation.getContext();
var identity = context.getIdentity();
var attributes = identity.getAttributes();
var email = attributes.getValue('email').asString(0);

if (identity.hasRealmRole('admin') || email.endsWith('@keycloak.org')) {
    $evaluation.grant();
}

在编写自己的规则时,请记住 $evaluate对象实现了org.keycloak.authority.policy.evaluate.evaluation。有关该接口的更多信息,请参见Evaluation API。

基于规则的策略

Drools策略目前仅仅是一个预览版而不完全支持。默认情况下该功能被禁用。

要启用,请在服务器启动命令中添加 -Dkeycloak.profile=preview 或 -Dkeycloak.profile.feature.authz_drools_policy=enabled。更多细节请查看这里。

使用这种类型的策略,你可以使用Drools来定义权限的条件。Drools可以视作一个规则评估环境。它是Keycloak支持的基于规则的策略类型之一,同样提供了编写任何策略的灵活性。

要创建基于规则的策略,请在创建策略的下拉列表中选择Rule

配置

  • 名称

名称是一个可读的字符串。我们强烈建议取一些和业务相关的名字以易于识别。

  • 描述

此策略的详情。

  • Policy Maven Artifact

这里需要提供一个Maven的GAV坐标,然后点击Resolve来加载ModuleSession

-   Group Id
-   Artifact Id
-   Version
  • 逻辑

在评估完此策略相关条件之后再应用的逻辑。

例子

下面是一个ABAC的Drools小例子,仅当申请方为资源拥有者时才授权。

import org.keycloak.authorization.policy.evaluation.Evaluation;
rule "Authorize Resource Owner"
    dialect "mvel"
    when
       $evaluation : Evaluation(
           $identity: context.identity,
           $permission: permission,
           $permission.resource != null && $permission.resource.owner.equals($identity.id)
       )
    then
        $evaluation.grant();
end

也可以从身份中获取一个属性并加以判断:

import org.keycloak.authorization.policy.evaluation.Evaluation;
rule "Authorize Using Identity Information"
    dialect "mvel"
    when
       $evaluation : Evaluation(
           $identity: context.identity,
           identity.attributes.containsValue("someAttribute", "you_can_access")
       )
    then
        $evaluation.grant();
end

更多关于Evaluation接口的信息请查看评估API。

基于时间的策略

此种策略允许你制定时间相关的权限条件。

要创建基于时间的策略,请在创建策略的下拉列表中选中Time

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • Not Before

    在此时间前不得授权。只有当前日期/时间晚于或等于此值时才授权。

  • Not On or After

    在此时间后不得授权。只有当前日期/时间早于此值时才授权。

  • Day of Month

    定义必须授予访问权限的日期。允许指定日期范围。此时,只有日期介于或等于指定的两个值之间时,才会授予权限。

  • Month

    定义必须授予访问权限的月份。允许指定月范围。此时,只有当月份介于指定的两个值之间时,才会授予权限。

  • Year

    定义必须授予访问权限的年份。允许年份范围。此时,只有当前年份位于或等于指定的两个值之间时,才会授予权限。

  • Hour

    定义必须授予访问权限的时间。允许指定小时范围。此时,只有当前时间介于指定的两个值之间时,才授予权限。

  • Minute

    定义必须授予访问的分钟数。允许指定分钟的范围。此时,只有当前时间的分钟数介于指定的两个值之间,才会授予权限。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

聚合策略

如前所述,Keycloak支持构建策略的策略,称之为聚合策略。你可以使用策略聚合,通过重用现有的策略来构建更复杂策略,使得权限与策略更解耦。

要创建聚合策略,请在创建策略时选中Aggregated

假设有一个名为 Confidential resource的资源,该资源只能由来自keycloak.org域和特定范围的IP地址的用户访问。你当然可以同时使用这两种条件创建一个策略,但你可能希望重用此策略的域部分,以便用于其它的对来源网络无要求的场景。

这时就可以为域和网络条件创建单独的策略,并基于这两个策略的组合创建第三个策略。

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • 应用策略

    定义要聚合的一个或者多个策略。这里可以选择已有的策略,也可以创建新策略。

  • 决策逻辑

    此权限的决策逻辑。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

决策逻辑

在创建聚合策略时还可以定义决策逻辑,它将用于根据各策略的结果确定最终决策。

  • 一致通过

默认逻辑。意味着必须满足全部策略。

  • 肯定的

要求至少一个策略是积极的,最终决策才是积极的。

  • 共识的

要求正面决策的数量必须大于负面决策的数量。如果正面和负面的数量相同,那么最终的决策将是负面的。

基于客户端的策略

可以使用这种类型的策略来定义权限条件。

要创建基于客户端的策略,请在创建策略时选择Client

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • 客户端

    定义此策略要授权的客户端。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

基于组的策略

此种策略允许只有你指定的组才能获取访问权限。

要创建基于组的策略,请在创建策略时选中Group

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • 组声明

    在包含组名和/或路径的令牌中指定声明(即令牌key/value中的key)。通常,授权请求是基于ID Token或Access Token处理的。该策略将根据Token的组声明来获取用户所属的组。如果没有定义,则从域配置中获取。

  • 在这里选择在评估策略时需要应用此策略的组。添加组之后,可以选中复选框Extend to Children来将访问权扩展到子组。如果没有标记,访问限制只适用于所选的组。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

扩展子组的访问权

默认情况下,访问限制将仅应用到配置的组内的成员。

在某些情况下,可能需要对父子结构的组统一授权。此时你可以选中Extend to Children复选框。

上面的图示的策略会对IT及其子组的成员统一授予访问权限。

积极逻辑和消极逻辑

策略可以配置为积极或消极。简单地说,可以使用这个选项来定义策略结果是应该保持原样,还是被否定。

举例来说,假设创建了策略,其中只有没有授予特定角色的用户才被授权。此时可以使用该角色创建基于角色的策略,并将其逻辑字段设置为消极(Negative)。

策略评估API

在使用JavaScript或Drools编写基于规则的策略时,Keycloak提供了评估API,它可以帮忙确定是否应该授予权限。

API提供了接口用来让开发者访问一些有用的信息,如:

  • 要评估的权限,代表请求的资源和范围。
  • 与请求资源所关联的属性
  • 与执行上下文相关的运行时环境及其属性
  • 用户相关的信息,如组以及角色

主要的接口是 org.keycloak.authorization.policy.evaluation.Evaluation,它的定义如下:

public interface Evaluation {

    /**
     * Returns the {@link ResourcePermission} to be evaluated.
     *
     * @return the permission to be evaluated
     */
    ResourcePermission getPermission();

    /**
     * Returns the {@link EvaluationContext}. Which provides access to the whole evaluation runtime context.
     *
     * @return the evaluation context
     */
    EvaluationContext getContext();

    /**
     * Returns a {@link Realm} that can be used by policies to query information.
     *
     * @return a {@link Realm} instance
     */
    Realm getRealm();

    /**
     * Grants the requested permission to the caller.
     */
    void grant();

    /**
     * Denies the requested permission.
     */
    void deny();
}

在处理授权请求时,Keycloak会在评估任意策略前创建一个Evaluation实例。然后将此实例传递给每个策略,以确定访问是授予还是拒绝

策略通过调用Evaluation实例上的 grant() 或 deny() 方法来确定是授权还是拒绝。默认情况下,Evaluation实例的状态是拒绝,这意味着策略必须显式地调用grant() 方法,以向策略评估引擎表明应该授权。

更多Evaluation API相关信息请查看这里。

评估上下文

评估上下文在评估的过程中向策略提供有用信息。

public interface EvaluationContext {

    /**
     * Returns the {@link Identity} that represents an entity (person or non-person) to which the permissions must be granted, or not.
     *
     * @return the identity to which the permissions must be granted, or not
     */
    Identity getIdentity();

    /**
     * Returns all attributes within the current execution and runtime environment.
     *
     * @return the attributes within the current execution and runtime environment
     */
    Attributes getAttributes();
}

策略可以从上面的接口中获取以下信息:

  • 已被认证通过的身份
  • 执行上下文和运行时环境的信息

身份信息基于OAuth2 Access Token来创建,创建过程会从原始Token提取所有声明。举例来说,如果你通过Protocal Mapper向OAuth2 Access Token添加了自定义的声明,评估策略时就可以获取它并利用它来构建授权条件。

EvaluationContext 也提供了访问执行及运行时环境的入口。但是目前只提供几个内置属性:

名称描述类型
kc.time.date_time当前日期和时间String.Format MM/dd/yyyy hh:mm:ss
kc.client.network.ip_address客户端的IPv4地址String
kc.client.network.host客户端主机名称String
kc.client.id客户端IDString
kc.client.user_agentHTTP头中User-Aggent的值String[]
kc.realm.name域名称String

管理权限

权限关联了被保护的对象和要评估的策略,以决定是否应该授予访问权。

创建了资源和策略之后,我们现在来管理权限。点击Permissions选项卡:

权限主要用来管理两类对象:

  • 资源
  • 范围

请在创建权限的下拉列表中选择想要创建的类型。下面我们分别讲述它们的细节。

创建基于资源的权限

基于资源的权限定义了一组资源且使用一组策略来保护这些它们。

要创建基于资源的权限,请在创建权限时选中 Resource-based

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • 此权限要作用的资源类型

    指定是否应用于指定类型的资源,如果选择了这个属性就需要填写资源类型。

    • 资源类型

    定义要保护的资源类型。Keycloak将对匹配此类型的所有资源评估此权限。

  • 资源

    此权限要保护的一组资源。

  • 策略

    定义此权限关联的一组策略。这里可以选择已有的策略或者新建。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

分类资源的权限

可以针对一组同类型的资源统一设定权限。在对有着相同访问限制的资源设置权限时非常有用。

通常可以根据项目中资源的数据或其提供的功能来分类。比如说在一个金融类的应用,其中每个银行账户属于一个特定的用户。虽然是不同的银行账户,但是可以共享全局银行机构通用的安全性要求。你可以使用分类的资源权限来定义这些通用策略,例如:

  • 仅有账户拥有者才能管理自己的账户
  • 仅能在账户拥有者的国家/地区进行访问
  • 执行特定的身份认证方法

要创建分类资源的权限,请在创建基于资源的权限时点击Apply to Resource Type。Apply to Resource Type开启后,你就可以使用此策略来保护同一类资源了。

创建基于范围的权限

基于范围的权限定义一组范围,通过关联授权策略来保护这些范围。与基于资源的权限不同,可以使用此权限类型为与资源关联的范围创建权限,从而提供更细的权限控制粒度。

要创建新的基于范围的权限,请在新建权限时选择Scoped-based

配置

  • 名称

    名称是一个可读的字符串。建议取一些和业务相关的名字以易于识别。

  • 描述

    此策略的详情

  • 资源

    如果定义了资源,定义的范围仅与这些资源关联的才有效。如果置空,则全部范围都有效。

  • 范围

    定义一组要保护的范围。

  • 策略

    定义此权限关联的一组策略。这里可以选择已有的策略或者新建。

  • 逻辑

    在评估完此策略相关条件之后再应用的逻辑。

策略决策逻辑

与组合策略一样,这里可以定义多个策略评估结果如何生效的最终逻辑。

  • 一致通过

默认逻辑。意味着必须满足全部策略。

  • 肯定的

要求至少一个策略是积极的,最终决策才是积极的。

  • 共识的

积极策略的数量必须大于消极策略的数量,数量相等时结果判定为消极。

评估以及测试策略

在设计策略时,你可能需要模拟测试策略的评估结果。

可以点击Evaluate选项卡来使用在线的策略评估工具。在这里你可以通过输入模拟请求来查看输出的评估结果。

输入身份信息

Identify Information过滤器用来选择发起请求的用户。

输入上下文信息

Contextual Infomation过滤器添加额外的属性,Keycloak使用这些属性来评估策略。

输入权限

Permissions过滤器用来构建授权请求。你可以为一组资源或者范围来请求权限。如果想对全体资源或范围进行测试,点击Add后不要输入任何信息。

当全部配置好之后,点击Evaluate开始评估。

授权服务

Keycloak授权服务构建在已经广泛使用的标准上,如OAuth2和UMA规范。

OAuth2客户端(如前端应用程序)可以利用token endpoint从服务器获取Access Token,然后使用Access Token从资源服务器(例如后端服务)获取被保护的资源。Keycloak授权服务扩展了OAuth2,通过评估与被请求的资源或范围相关联的策略来发出Access Token。这意味着资源服务器可以基于含有权限Access Token来控制对资源的访问。在Keycloak授权服务中,这种具有权限的Access Token称为请求方令牌或简称RPT。

除了发布RPTs之外,Keycloak授权服务还提供了一组RESTful端点,这些端点允许资源服务器管理它们受保护的资源、范围、权限和策略,帮助开发人员将这些功能集成到应用程序中,以支持细粒度授权。

授权服务端点发现和元数据

Keycloak提供了一个发现文档来帮助客户端获取与Keycloak授权服务交互所需的任何信息,包括端点位置和功能。

可以从这里获取发现文档:

curl -X GET \
  http://${host}:${port}/auth/realms/${realm}/.well-known/uma2-configuration

请将上面占位符中的变量替换为实际的值。

你收到的响应应该是如下类型:

{

    // some claims are expected here

    // these are the main claims in the discovery document about Authorization Services endpoints location
    "token_endpoint": "http://${host}:${post}/auth/realms/${realm}/protocol/openid-connect/token",
    "token_introspection_endpoint": "http://${host}:${post}/auth/realms/${realm}/protocol/openid-connect/token/introspect",
    "resource_registration_endpoint": "http://${host}:${post}/auth/realms/${realm}/authz/protection/resource_set",
    "permission_endpoint": "http://${host}:${post}/auth/realms/${realm}/authz/protection/permission",
    "policy_endpoint": "http://${host}:${post}/auth/realms/${realm}/authz/protection/uma-policy"
}

每个端点都暴露了一组功能:

  • token_endpoint

遵循OAuth2的Token端点支持 urn:ietf:params:oauth:grant-type:uma-ticket 授权类型。客户端可以向此端点发送授权申请并获取Keycloak RPT。

  • token_introspection_endpoint

遵循Oauth2的Token检查端点。客户端可以使用该端点查询RPT的状态,并确定与token相关的任何其他信息,比如Keycloak授予的权限。

  • resource_registration_endpoint

遵循UMA协议的资源注册端点,资源服务器可以使用它来管理资源以及范围。此端点提供了资源、范围的创建、查询、更新以及删除等功能。

  • permission_endpoint

遵循UMA协议的权限管理端点,资源服务器可以用来管理权限tickets。提供了对permission ticket的创建、查询、更新以及删除等功能。

获取权限

要从Keycloak获取权限,需要向token端点发送授权请求。Keycloak会根据申请的资源以及范围来评估关联策略,最后发送携带权限的RPT。

客户端发送的授权申请可以有下列参数:

  • grant_type

必填。必须是 urn:ietf:params:oauth:grant-type:uma-ticket。

  • ticket

可选。UMA授权过程中,客户端最近一次收到的权限ticket。

  • claim_token

可选。Keycloak评估授权时用到的附加的声明。它允许客户端推送声明到Keycloak。更多细节请查看token格式中的 cliam_token_format 参数。

  • claim_token_format

可选。用于说明claim_token参数的格式。Keycloak支持两种token格式:urn:ietf:params:oauth:token-type:jwt 以及 https://openid.net/specs/openid-connect-core-1_0.html#IDToken。urn:ietf:params:oauth:token-type:jwt 格式表示claim_token参数引用了一个access token。https://openid.net/specs/openid-connect-core-1_0.html#IDToken 则表示claim_token参数引用了一个OpenID Connect ID Token。

  • rpt

可选。代表一个以前发放的RPT,其权限将被评估并添加到一个新的RPT中。此参数允许拥有RPT的客户端执行增量授权。

  • permission

可选。表示客户端正在寻求访问的一组资源以及范围。此参数可以重复定义,以请求针对多个资源和范围的权限。这个参数扩展了 urn:ietf:params:oauth:grant-type: uml -ticket 授权类型,以允许客户端在没有权限ticket的情况下发送授权请求。字符串的格式必须是:RESOURCE_ID#SCOPE_ID。例如:Resource A#Scope A, Resource A#Scope A, Scope B, Scope C, Resource A,#Scope A。

  • audience

可选。正在访问的资源服务器的客户端标识符。如果定义了permission参数,则必须填此参数。它用作Keycloak计算权限的上下文提示。

  • response_include_resource_name

可选。指示RPT的权限中是否包含资源名称。如果为false,则只包含资源ID。

  • response_permissions_limit

可选。一个整型的值N,用来表示RPT最多可携带的权限个数。和rpt参数一起使用,只有最近N次请求的权限会被保留在RPT中。

  • submit_request

可选。布尔型,用来指示服务器是否要创建权限ticket相关资源/范围的权限请求。此参数只有在UMA授权过程中与ticket参数一起使用时才有效。

  • response_mode

可选。指示服务器如何响应授权请求。可以让服务器仅返回决策结果或者权限列表而不是完整的标准OAuth2响应,此参数可选的值为:

        -   decision

指示服务器仅发送决策结果:  

{
    'result': true
}

如果授权请求没有映射到任何资源,则返回403状态码。

        -   permissions

指示服务器返回权限列表:

[
    {
        'rsid': 'My Resource'
        'scopes': ['view', 'update']
    },

    ...
]

如果授权请求没有映射任何权限,将返回403状态码。

下面的例子展示了客户端申请访问资源服务器保护的两个资源:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "audience={resource_server_client_id}" \
  --data "permission=Resource A#Scope A" \
  --data "permission=Resource B#Scope B"

下面的例子展示了客户端请求访问受保护的任意资源和范围:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "audience={resource_server_client_id}"

下面的例子展示了客户端在从资源服务器接收到权限ticket后,继续请求受UMA保护的资源:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "ticket=${permission_ticket}

Keycloak会根据评估结果发放RPT:

HTTP/1.1 200 OK
Content-Type: application/json
...
{
    "access_token": "${rpt}",
}

RPT可以从access_token响应参数中获取。如果客户端未通过授权评估,Keycloak将返回403 HTTP状态码:

HTTP/1.1 403 Forbidden
Content-Type: application/json
...
{
    "error": "access_denied",
    "error_description": "request_denied"
}

客户端验证方式

客户端需要身份验证才能获取RPT。当使用 urn:ietf:params:oauth:grant-type:uma-ticket 授权类型时,客户端可以使用以下任何一种身份验证方法:

  • Bearer Token

此时发送给token端点的请求必须带有Bearer的HTTP Authorization头。

下面的例子展示了如何使用access token对客户端进行身份认证。

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"

当客户端代表用户执行操作时,上边的方法特别有用。上面的Bearer token是Keycloak已经向(代表用户的)客户端发放的access token。Keycloak将根据access token所代表的上下文来评估权限。即,如果access token是向(代表用户A的)客户端A发放的,那么Keycloak将根据用户A来授予权限。

  • Client Credentials

客户端可以使用Keycloak提供的任意方式来认证。如 client_id/client_secret 或者JWT。

下面的例子展示了如何在请求中附带client id以及client secret来验证:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Basic cGhvdGg6L7Jl13RmfWgtkk==pOnNlY3JldA==" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"

推送声明

在从服务端获取权限的过程中,你可以向服务端提供任意的声明,用于向权限评估提供信息。

如果不使用权限ticket(即不适用UMA)来获取权限,你可以发送下面的授权请求:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "claim_token=ewogICAib3JnYW5pemF0aW9uIjogWyJhY21lIl0KfQ==" \
  --data "claim_token_format=urn:ietf:params:oauth:token-type:jwt" \
  --data "client_id={resource_server_client_id}" \
  --data "client_secret={resource_server_client_secret}" \
  --data "audience={resource_server_client_id}"

claim_token参数是一个BASE64编码的JSON,编码前的格式如下:

{
    "organization" : ["acme"]
}

格式中可以有一个或多个声明,其中每个声明的值都是一个字符串数组。 

使用UMA推送声明

有关使用UMA和权限ticket时如何推送声明的更多细节,请查看Permission API。

User-Managed Access

Keycloak 授权服务是基于User-Managed Access的,我们简写为UMA。UMA协议在下面这些方面增强了OAuth2:

  • 隐私

如今,越来越多的数据和设备连接到云,用户隐私也随之成为一个巨大的问题。资源服务器可以使用UMA和Keycloak来增强它们的功能,以便改进它们保护用户隐私方面的方式,根据用户定义的策略授予权限。

  • 点对点授权

资源所有者(如终端用户)可以管理对其资源的访问,并授权其他人(如另一个终端用户)访问这些资源。这与OAuth2不同,在OAuth2中,被授予许可的是代表用户的客户端。而UMA允许资源所有者对其他用户本身进行异步授权。

  • 资源共享

资源所有者可以管理其资源的权限,并决定谁可以访问特定的资源以及如何访问。Keycloak作为共享管理服务,资源所有者在其上来管理自己的资源。

Keycloak符合UMA 2.0规范,提供了大部分UMA功能。

举例来说,用户Alice(资源所有者)使用互联网银行服务(资源服务器)管理他的银行帐户(资源)。有一天,Alice决定把她的银行账户开给一个会计专业人士Bob(请求方)。但是,Bob应该只能查看(范围)Alice的帐户。

作为资源服务器,互联网银行服务必须保护Alice的银行帐户。为此,它依赖于Keycloak资源注册端点在服务器中创建一个代表Alice银行账户的资源。

如果Bob此时试图访问Alice的银行帐户,访问将被拒绝。互联网银行服务为银行帐户定义了一些默认策略。如只有所有者(Alice)才能访问自己的银行账户。

网上银行服务考虑到Alice的隐私,因此允许她更改自己账户的策略。如允许哪些人查看其银行账户。为此,互联网银行服务基于Keycloak让Alice自行选择允许谁来访问她的数据以及能做出哪些操作。在任何时候,Alice都可以对Bob撤销或新增额外授权。

授权过程

在UMA中,授权过程开始于客户端试图访问受UMA保护的资源服务器。

受UMA保护的资源服务器要求请求带有bearer token。下面模拟了不带权限ticket的请求:

curl -X GET \
  http://${host}:${port}/my-resource-server/resource/1bfdfe78-a4e1-4c2d-b142-fc92b75b986f

资源服务器将一个响应发送回客户机,该响应带有一个权限ticket和一个as_uri参数,该as_uri表示Keycloak服务器的地址,客户端可以将ticket发送到此地址来获得RPT。

下面是资源服务器的响应,注意响应中包含权限ticket:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: UMA realm="${realm}",
    as_uri="https://${host}:${port}/auth/realms/${realm}",
    ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de"

权限ticket是Keycloak权限API发放的一种特殊token。它们代表被申请的权限(如资源以及范围)以及其它请求中的关联信息。只有资源服务器可以创建它们。

客户机已经拥有了权限ticket和Keycloak服务器的地址,就可以使用文档发现API来获取token endpoint的地址并发送授权请求。

下面的例子展示了客户端如何向token endpoint请求RPT:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "ticket=${permission_ticket}

Keycloak评估通过后会发放权限相关的RPT:

HTTP/1.1 200 OK
Content-Type: application/json
...
{
    "access_token": "${rpt}",
}

响应和其他授权类型的响应类似。RPT在access_token响应参数中,如果client没有通过授权服务端会返回403状态码:

HTTP/1.1 403 Forbidden
Content-Type: application/json
...
{
    "error": "access_denied",
    "error_description": "request_denied"
}

提交权限请求

在整个授权过程中,客户端首先需要从受UMA保护的资源服务器获得一个权限ticket,以便在Keycloak的token端点用它来交换RPT。

如果Keycloak判断客户端无法通过授权,会返回403及request_denied错误信息。

HTTP/1.1 403 Forbidden
Content-Type: application/json
...
{
    "error": "access_denied",
    "error_description": "request_denied"
}

上面的响应表示Keycloak不能对该权限ticket发放RPT。

某些时候客户端会用到异步授权流程,并让被资源所有者来决定是否授权。为此,客户端可以在对token端点的请求中带上submit_request参数:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm}/protocol/openid-connect/token \
  -H "Authorization: Bearer ${access_token}" \
  --data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
  --data "ticket=${permission_ticket} \
  --data "submit_request=true"

当使用submit_request参数时,Keycloak会记录下每个被拒绝的权限申请。然后资源所有者就可以检查并管理这些它们。

你可以将此功能想象为APP中那些请求访问的按钮,用户访问其他用户的资源前需要先向他们申请。

管理对用户资源的访问

用户可以使用Keycloak来管理对其资源的访问。要启用此功能,必须首先启用域的用户管理访问,请打开Keycloak管理控制台中的Realm设置页面,并启用用户管理的访问开关。然后进入account客户端:

Account客户端的左侧的菜单栏会展示My Resources选项,用户可以在这里:

  • 管理需要同意授权的请求

包含等待批准的所有权限请求列表。每个请求都关联到请求访问特定资源的用户。用户可以批准或拒绝这些请求。

  • 管理我的资源

包含当前用户的所有资源。用户可以点击资源来查看详情或者把它们共享给其他用户。

  • 管理其他人共享给我的资源

包含一个共享给当前用户的资源列表。

  • 管理等待审批的请求

包含一个等待当前用户(资源所有者)审批的请求列表。

当用户点击 “我的资源” 列表中的任何资源详情时,将被重定向到如下页面:

用户可以在这里:

  • 管理允许访问此资源的用户

在这里可以查看所有有权访问此资源的用户。可以通过点击 Revoke 来收回他们的访问权。

  • 将资源共享给其他人

可以在这里输入用户名或者邮箱,来选择为特定用户开放权限。

保护API

保护API遵循UMA提供了一系列端点:

  • 资源管理

用户可以用此端点来远程管理资源,并开启策略执行器来向服务器查询资源的访问权限。

  • 权限管理

UMA协议中,资源服务器可以访问此端点来获取权限ticket。此端点还支持查询权限以及管理权限状态。

  • 策略API

Keycloak利用UMA保护API使资源服务器能够管理各自用户的权限。除了资源和权限API外,Keycloak还提供了策略API。可以让资源服务器代表其用户设置资源的权限。

使用此API要求资源服务器拥有名为protection API token(PAT)的特殊OAuth2 access token。在UMA中,PAT是有着uma_protection范围的token。

什么是PAT,如何获取它们?

一个protection API token(PAT)是有着uma_protection范围的特殊token。创建资源服务器时Keycloak会自动创建uma_protection的角色,并将其与客户端的服务帐户关联起来。

资源服务器可以像获取其他OAuth2 token那样从Keycloak获取PAT。下边是一个curl的例子:

curl -X POST \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d 'grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}' \
    "http://localhost:8080/auth/realms/${realm_name}/protocol/openid-connect/token"

上面的示例使用client_credentials授权类型从服务器获取PAT。服务器返回的响应如下:

{
  "access_token": ${PAT},
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": ${refresh_token},
  "token_type": "bearer",
  "id_token": ${id_token},
  "not-before-policy": 0,
  "session_state": "ccea4a55-9aec-4024-b11c-44f6f168439e"
}

Keycloak可以使用不同的方式来认证客户端应用。上面使用了 client_credentials 方式,它要求提供一个client_id和一个client_secret。你可以自由选择其它方式来认证。

管理资源

Keycloak提供了遵循UMA协议的端点来远程管理资源。

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set

下面列出了此端点的主要功能(为了清晰起见省略了全路径):

  • 创建资源: POST /resource_set
  • 读取资源:GET /resource_set/{_id}
  • 更新资源: PUT /resource_set/{_id}
  • 删除资源:DELETE /resource_set/{_id}
  • 资源列表:GET /resource_set

更多信息请查看UMA资源注册API。

创建资源

使用下面的POST请求来创建资源:

curl -v -X POST \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '{
     "name":"Tweedl Social Service",
     "type":"http://www.example.com/rsrcs/socialstream/140-compatible",
     "icon_uri":"http://www.example.com/icons/sharesocial.png",
     "resource_scopes":[
         "read-public",
         "post-updates",
         "read-private",
         "http://www.example.com/scopes/all"
      ]
  }'

默认情况下,资源的所有者是资源服务器。可以使用下面的请求来定义其他资源所有者:

curl -v -X POST \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '{
     "name":"Alice Resource",
     "owner": "alice"
  }'

其中owner字段开始是用户名或者用户唯一标识。

创建用户管理的资源

默认情况下,通过保护API创建的资源不能由资源所有者通过帐户服务来管理。

要创建资源所有者可管理的资源,你必须设置ownerManagedAccess属性:

curl -v -X POST \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '{
     "name":"Alice Resource",
     "owner": "alice",
     "ownerManagedAccess": true
  }'

更新资源

使用HTTP PUT请求来更新资源:

curl -v -X PUT \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set/{resource_id} \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '{
     "_id": "Alice Resource",
     "name":"Alice Resource",
     "resource_scopes": [
        "read"
     ]
  }'

删除资源

使用HTTP DELETE请求来删除资源:

curl -v -X DELETE \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set/{resource_id} \
  -H 'Authorization: Bearer '$pat

查询资源

通过ID查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set/{resource_id}

通过name来查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set?name=Alice Resource

通过URI来查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set?uri=/api/alice

通过owner查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set?owner=alice

通过type查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set?type=albums

通过scope查询资源:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/resource_set?scope=read

 查询时可以通过first和max来限制结果条数。

管理权限申请

使用UMA的资源服务器可以使用端点来管理权限申请。端点也提供了UMA兼容的流程来注册权限请求以及获取权限ticket。

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/permission

上面说到的权限ticket是一种特殊的token,代表了一个权限申请。UMA规范对权限ticket的定义如下:

它代表着一个相关句柄,从授权服务器传递到资源服务器、再到客户端,最终回到授权服务器。授权服务器在授权过程中基于此句柄来选择要评估的策略。

在大多数情况下,你不需要直接调用此端点。Keycloak为资源服务器提供了策略执行器,用来从授权服务器获取权限ticket,再将此ticket返回客户端应用,并且基于最终的RPT执行授权决策。

从Keycloak获取权限ticket的过程是由资源服务器执行,而不是由常规客户端执行的。在常规的客户端中,当客户端试图访问受保护的资源而没有访问该资源的必要授权时,就会获得权限ticket。签发权限ticket是UMA的一个重要方面,它使得资源服务器:

  • 将资源服务器要保护的资源相关的数据从客户端抽象出来
  • 在Keycloak中注册授权请求,使得资源所有者的可以管理是否同意授权
  • 将资源服务器和授权服务器解耦,使得资源服务器可以选择不同的授权服务器来管理资源

从客户端的角度来看,权限ticket也有着重要作用:

  • 客户端无需知道授权和资源是如何关联的。权限ticket对客户端来说是不透明的。
  • 客户端可以访问不同资源服务器上被不同授权服务器所保护的资源。

这些只是UMA带来的一些好处,其实UMA主要优势在隐私和用户控制资源访问方面。

创建权限ticket

下面的HTTP POST请求展示了如何创建权限Ticket:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/permission \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '[
  {
    "resource_id": "{resource_id}",
    "resource_scopes": [
      "view"
    ]
  }
]'

你可以在上面的请求中添加任意的声明:

curl -X POST \
  http://${host}:${port}/auth/realms/${realm_name}/authz/protection/permission \
  -H 'Authorization: Bearer '$pat \
  -H 'Content-Type: application/json' \
  -d '[
  {
    "resource_id": "{resource_id}",
    "resource_scopes": [
      "view"
    ],
    "claims": {
        "organization": ["acme"]
    }
  }
]'

这些声明将用于评估此权限ticket相关的策略。 

使用策略API来管理资源权限

Keycloak利用UMA保护API来允许资源服务器管理其用户的资源。除了资源以及权限API之外,Keycloak还提供了策略API,来让资源服务器代表其用户设置资源权限。

策略服务端点如下:

http://${host}:${port}/auth/realms/${realm_name}/authz/protection/uma-policy/{resource_id}

访问这些API时需要携带bearer token,来证明用户允许资源服务器替代自己来管理权限。bearer token可以是从token端点获取到的常规token:

  • 资源所有者密码验证授权
  • 交换令牌,即将授予某些客户端(公共客户端)的access token交换为资源服务器的bearer token

将权限与资源关联

用下面的POST请求来将权限关联到特定资源:

curl -X POST \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{resource_id} \
  -H 'Authorization: Bearer '$access_token \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Any people manager",
        "description": "Allow access to any people manager",
        "scopes": ["read"],
        "roles": ["people-manager"]
}'

上面的例子中,我们创建了一个权限,并将它关联到指定 resource_id 的资源,表示有着 people-manager 角色的用户都被授予 read 范围。

下面展示了一个新建组访问控制策略的例子:

curl -X POST \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{resource_id} \
  -H 'Authorization: Bearer '$access_token \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Any people manager",
        "description": "Allow access to any people manager",
        "scopes": ["read"],
        "groups": ["/Managers/People Managers"]
}'

下面使用了客户端访问的策略:

curl -X POST \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{resource_id} \
  -H 'Authorization: Bearer '$access_token \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Any people manager",
        "description": "Allow access to any people manager",
        "scopes": ["read"],
        "clients": ["my-client"]
}'

甚至可以使用JavaScript来自定义策略:

curl -X POST \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{resource_id} \
  -H 'Authorization: Bearer '$access_token \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Any people manager",
        "description": "Allow access to any people manager",
        "scopes": ["read"],
        "condition": "if (isPeopleManager()) {$evaluation.grant()}"
}'

也可以组合不用的访问控制机制。

使用HTTP PUT请求来更新权限:

curl -X PUT \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{permission_id} \
  -H 'Authorization: Bearer '$access_token \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "21eb3fed-02d7-4b5a-9102-29f3f09b6de2",
    "name": "Any people manager",
    "description": "Allow access to any people manager",
    "type": "uma",
    "scopes": [
        "album:view"
    ],
    "logic": "POSITIVE",
    "decisionStrategy": "UNANIMOUS",
    "owner": "7e22131a-aa57-4f5f-b1db-6e82babcd322",
    "roles": [
        "user"
    ]
}'

删除权限

使用下面的DELETE请求来删除特定的权限:

curl -X DELETE \
  http://localhost:8180/auth/realms/photoz/authz/protection/uma-policy/{permission_id} \
  -H 'Authorization: Bearer '$access_token

查询权限

查询某资源关联的所有权限:

http://${host}:${post}/auth/realms/${realm}/authz/protection/uma-policy?resource={resource_id}

使用权限名称查询权限:

http://${host}:${post}/auth/realms/${realm}/authz/protection/uma-policy?resource={resource_id}

查询关联 read 范围的权限:

http://${host}:${post}/auth/realms/${realm}/authz/protection/uma-policy?scope=read

查询全部权限:

http://${host}:${post}/auth/realms/${realm}/authz/protection/uma-policy

可以使用first以及max参数来限定查询结果。

RPT

RPT是使用JSON web signature(JWS) 签发的JSON web token(JWT)。RPT基于之前由Keycloak发出的OAuth2 access token来构建,该token用于用户自身或代表用户的特定客户端。

解码RPT可以看到它的payload如下:

{
  "authorization": {
      "permissions": [
        {
          "resource_set_id": "d2fe9843-6462-4bfc-baba-b5787bb6e0e7",
          "resource_set_name": "Hello World Resource"
        }
      ]
  },
  "jti": "d6109a09-78fd-4998-bf89-95730dfd0892-1464906679405",
  "exp": 1464906971,
  "nbf": 0,
  "iat": 1464906671,
  "sub": "f1888f4d-5172-4359-be0c-af338505d86c",
  "typ": "kc_ett",
  "azp": "hello-world-authz-service"
}

从此token中,你可以从服务器获取所有 permissions 声明的权限。

注意,权限与要保护的资源/范围直接相关,并且完全与实际授予和发出这些权限的机制解耦。

审视RPT

有些时候可能需要检视RPT来查看有效性或者从中获取权限,以便让资源服务器来强制执行授权决策。

有两种检视RPT的主要场景:

  • 客户端应用需要检查token有效性来获取一个新的token
  • 在资源服务器端执行授权决策,特别是没有内置的策略执行器能够满足你的要求时

获取RPT信息

你可以从下面的端点来获取RPT信息,它满足OAuth token 检视规范:

http://${host}:${port}/auth/realms/${realm_name}/protocol/openid-connect/token/introspect

要使用上面的端点来检视一个RPT,你可以发送如下请求:

curl -X POST \
    -H "Authorization: Basic aGVsbG8td29ybGQtYXV0aHotc2VydmljZTpzZWNyZXQ=" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d 'token_type_hint=requesting_party_token&token=${RPT}' \
    "http://localhost:8080/auth/realms/hello-world-authz/protocol/openid-connect/token/introspect"

上面的请求使用了HTTP BASIC认证,并且发送了客户端的client ID以及secret。用于审核申请检视RPT的客户端身份,你也可以选择Keycloak支持的其它客户端认证方式。

请求中需要两个参数:

  • token_type_hint

此参数传值 requesting_party_token 代表你期望检视一个RPT。

  • token

使用授权过程中服务器传回的token传值。

服务端对上面请求的响应如下:

{
  "permissions": [
    {
      "resource_id": "90ccc6fc-b296-4cd1-881e-089e1ee15957",
      "resource_name": "Hello World Resource"
    }
  ],
  "exp": 1465314139,
  "nbf": 0,
  "iat": 1465313839,
  "aud": "hello-world-authz-service",
  "active": true
}

如果RPT处于过期状态,响应会直接返回:

{
  "active": false
}

每次检视RPT时都需要调用服务端吗?

不是的。默认情况下RPT也是符合JWT规范的token,这意味着你完全可以自己解释它。

如果希望在不调用远程检视端点的情况下验证这些token,可以本地解码RPT并检查有效性。解码之后,还可以使用token中的权限来执行授权决策。

这正是策略执行器做的事情,请记得:

  • 用realm的公钥来检查RPT签名
  • 根据其exp、iat和aud声明查询token有效性

授权客户端的Java API

根据实际项目的需求,资源服务器客远程管理资源以及通过编程来检查权限。如果你正在使用Java,则可以使用授权客户端API来访问Keycloak授权服务。

Keycloak针对资源服务器的不同需求,提供了不同端点(如token端点、资源端点和权限管理端点)。

Maven依赖

<dependencies>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-authz-client</artifactId>
        <version>${KEYCLOAK_VERSION}</version>
    </dependency>
</dependencies>

配置

客户端配置在keycloak.json文件中,下面是一个简单的例子:

{
  "realm": "hello-world-authz",
  "auth-server-url" : "http://localhost:8080/auth",
  "resource" : "hello-world-authz-service",
  "credentials": {
    "secret": "secret"
  }
}
  • realm(必须)

realm的名称。

  • auth-server-url(必须)

Keycloak地址。也是所有Keycloak页面以及REST符端点的根路径。通常是 https://host:port/auth 的形式。

  • resource(必须)

客户端ID。即客户端在Keycloak上的唯一标识。

  • 客户端身份(必须)

客户端凭据。这是一个键值对,其中键是凭据类型,值是该类型对应的值。

一般情况下该配置文件应该放在项目的classpath中,这也是客户端寻找keycloak.json文件的默认路径。

创建授权客户端

现在假定classpath中已经有了keycloak.json,现在来创建一个AuthzClient:

    // create a new instance based on the configuration defined in a keycloak.json located in your classpath
    AuthzClient authzClient = AuthzClient.create();

获取用户授权

下面的例子展示了如果获取用户授权:

// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();

// create an authorization request
AuthorizationRequest request = new AuthorizationRequest();

// send the entitlement request to the server in order to
// obtain an RPT with all permissions granted to the user
AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
String rpt = response.getToken();

System.out.println("You got an RPT: " + rpt);

// now you can use the RPT to access protected resources on the resource server

下面的例子说明了如何为一组资源获取用户权限:

// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();

// create an authorization request
AuthorizationRequest request = new AuthorizationRequest();

// add permissions to the request based on the resources and scopes you want to check access
request.addPermission("Default Resource");

// send the entitlement request to the server in order to
// obtain an RPT with permissions for a single resource
AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize(request);
String rpt = response.getToken();

System.out.println("You got an RPT: " + rpt);

// now you can use the RPT to access protected resources on the resource server

使用保护API来创建资源

// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();

// create a new resource representation with the information we want
ResourceRepresentation newResource = new ResourceRepresentation();

newResource.setName("New Resource");
newResource.setType("urn:hello-world-authz:resources:example");

newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view"));

ProtectedResource resourceClient = authzClient.protection().resource();
ResourceRepresentation existingResource = resourceClient.findByName(newResource.getName());

if (existingResource != null) {
    resourceClient.delete(existingResource.getId());
}

// create the resource on the server
ResourceRepresentation response = resourceClient.create(newResource);
String resourceId = response.getId();

// query the resource using its newly generated id
ResourceRepresentation resource = resourceClient.findById(resourceId);

System.out.println(resource);

检查RPT

// create a new instance based on the configuration defined in keycloak-authz.json
AuthzClient authzClient = AuthzClient.create();

// send the authorization request to the server in order to
// obtain an RPT with all permissions granted to the user
AuthorizationResponse response = authzClient.authorization("alice", "alice").authorize();
String rpt = response.getToken();

// introspect the token
TokenIntrospectionResponse requestingPartyToken = authzClient.protection().introspectRequestingPartyToken(rpt);

System.out.println("Token status is: " + requestingPartyToken.getActive());
System.out.println("Permissions granted by the server: ");

for (Permission granted : requestingPartyToken.getPermissions()) {
    System.out.println(granted);
}

策略执行器

策略实施点(Policy Enforcement Point, PEP)是一种设计模式,可以由不同的方式实现。Keycloak提供了为不同平台、环境和编程语言实现PEPs的所有必要方法。Keycloak授权服务提供了一个RESTful API,并利用OAuth2授权功能使用集中式授权服务器进行细粒度授权。

PEP负责强制执行(Keycloak服务器决定的)访问决策。它在应用程序中充当过滤器或拦截器,以检查是否可以根据这些决策的授权来访问受保护的资源。

权限的执行取决于使用的协议。当使用UMA时,策略执行器使用RPT作为bearer token来发起请求。这意味着客户端在向资源服务器发送权限申请之前,应该首先从Keycloak获取一个RPT。

如果没有使用UMA,你就可以使用一个普通的access token。这种情况下,策略执行器会尝试从授权服务器直接获取权限。

如果你正在使用任何Keycloak OIDC适配器,就可以通过向keycloak.json文件添加以下属性来启用策略执行器:

{
 "policy-enforcer": {}
}

当您启用策略强制器时,所有发送到你的应用的请求都会被拦截,并根据Keycloak所授予的权限来检查是否能够访问。

策略的强制执行和Keycloak创建的资源以及项目项目路径强关联。默认情况下,创建资源服务器时,Keycloak会为资源服务器创建一个默认配置,这样就可以快速地启用策略强制执行。

配置

上面提高过,在keycloak.json文件中添加policy-enforcer来启动策略执行:

{
  "policy-enforcer": {}
}

如果要手动定义资源,可以写的复杂一点:

{
  "policy-enforcer": {
    "user-managed-access" : {},
    "enforcement-mode" : "ENFORCING"
    "paths": [
      {
        "path" : "/someUri/*",
        "methods" : [
          {
            "method": "GET",
            "scopes" : ["urn:app.com:scopes:view"]
          },
          {
            "method": "POST",
            "scopes" : ["urn:app.com:scopes:create"]
          }
        ]
      },
      {
        "name" : "Some Resource",
        "path" : "/usingPattern/{id}",
        "methods" : [
          {
            "method": "DELETE",
            "scopes" : ["urn:app.com:scopes:delete"]
          }
        ]
      },
      {
        "path" : "/exactMatch"
      },
      {
        "name" : "Admin Resources",
        "path" : "/usingWildCards/*"
      }
    ]
  }
}

下面来解释这个配置文件:

  • policy-enforcer

定义如何实际执行策略,以及希望保护的路径。如果没有指定,策略强制器将查询服务器,以查找与受保护的资源服务器相关联的所有资源。在这种情况下,需要确保资源的URIS被正确配置。

        - user-managed-access

指定适配器使用UMA协议。如果指定了,适配器将向服务器查询权限ticket,并根据UMA规范将它们返回给客户端。如果没有指定,则策略强制器将基于常规access token或RPT来执行权限。在这种情况下,拒绝访问资源前策略执行器将尝试直接从服务器获得权限。

        - enforcement-mode

指定策略如何强制执行。

            □ ENFORCING

            这是默认模式,当没有策略关联到指定资源时,请求会被拒绝。

            □ PERMISSIVE

            当没有策略关联到指定资源时,请求会被允许。

            □ DISABLE

            完全禁用策略评估,允许访问任何资源。禁用后,应用程序仍然能够通过授权上下文获得Keycloak授予的所有权限。

        - on-deny-reirect-to

定义一个URL,用于拒绝访问时重定向客户端请求。默认情况下,适配器使用403 HTTP状态码进行响应。

        - path-cache

定义策略执行器应如何跟踪应用中的路径及Keycloak的资源之的关联。通过这些配置关联,可以避免到Keycloak的不必要请求。

            □ lifespan

            本地缓存的过期时间,默认为3000,设置为0或者以下的值会禁用缓存。

            □ max-entries

            定义本地缓存的条数,如果没有提供时默认为1000条。

        - paths

指定要保护的路径。此配置是可选的。如果没有定义,策略执行器将查询Keycloak来获取本客户端对应的全部路径,并使用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:关联的范围数组。当范围与特定method关联时,试图访问受保护资源的客户端必须提供一个RPT用来检查范围权限。例如定义了 create 范围来表示 POST,则RPT必须包含对此路径的 create 范围的访问权限。

                scopes-enforcement-mode:范围的执行模式。值可以是ALLANY。如果是ALL,则必须授权给所有已定义的范围。如果是ANY,至少应该授权给一个范围。默认情况下,强制模式设置为ALL

            □ enforcement-mode

            指定策略如何执行

                ENFORCING:默认模式,没有policy关联的资源拒绝方案。

                DISABLED:禁用。

            □ claim-information-point

            定义一组个声明,这些声明必须被解析并推送到Keycloak服务器,以便策略使用。有关详细信息,请参见这里。

        - lazy-load-paths

指定适配器应如何获取与应用路径关联的资源的服务器。如果为 true,则策略执行器将根据请求的路径按需获取资源。当不想在部署期间从服务器获取所有资源(没有提供路径时),或者只定义了一个子路径集,并且希望按需获取其它路径时,这种配置特别有用。

        - http-method-as-scope

指定范围应如何映射到HTTP方法。如果设置为 true,策略执行器将使用当前请求中的HTTP方法来检查是否应该授予访问权限。启用时,请确保Keycloak配置的资源范围与要保护的路径HTTP方法正确对应。

        - claim-information-point

定义一组全局声明,这些声明必须被解析并推送到Keycloak服务器,以便策略评估使用。有关详细信息,请参见这里。

声明信息点

声明信息点(Claim Informatica Point, 简写为CIP)负责处理声明并推到Keycloak服务器,以便提供关于策略上下文的更多信息。可以将它们定义为策略执行器的配置选项,以便处理来自不同来源的声明,例如:

  • HTTP请求(参数、header、body 等)
  • 外部HTTP服务
  • 配置中的静态值
  • 实现了Claim Information Provider SPI的任意资源

当将声明推送到Keycloak服务器时,策略不仅可以基于当前用户,还可以基于给定事务的人、事务、时间、地点等等内容来作为运行上下文考虑并做出决策。即如何使用运行时信息来支持细粒度的授权决策。

从HTTP请求中获取信息

下面的keycloak.json配置说明了如何从HTTP请求中来解析信息:

"policy-enforcer": {
    "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服务获取信息

"policy-enforcer": {
    "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']}"
            }
          }
        }
      }
    ]
  }

静态声明

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

Cliam Informatica Provider 的SPI

当内置CIP不能满足实际需求时,开发人员可以使用SPI来支持不同的CIP。

例如,要实现一个新的CIP提供者,需要实现 org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory 以及ClaimInformationPointProvide,并且在项目的classpath中提供 META-INF/services/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一致,Keycloak通过此名称定位到实现。

当处理请求时,策略执行器将调用MyClaimInformationPointProviderFactory.create 方法,来获取MyClaimInformationPointProvider实例。调用时,此CIP provider定义的任何配置(通过CIP)都将传给Keycloak。

下面是一个Provider的例子:

public class MyClaimInformationPointProvider implements ClaimInformationPointProvider {

    private final Map<String, Object> config;

    public ClaimsInformationPointProvider(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
    KeycloakSecurityContext keycloakSecurityContext =
        (KeycloakSecurityContext) request
            .getAttribute(KeycloakSecurityContext.class.getName());
    AuthorizationContext authzContext =
        keycloakSecurityContext.getAuthorizationContext();

获取KeycloakSecurityContext的信息视具体的适配器而定,请查看运行平台的适配器配置。上面的例子适用于servlet容器。

授权上下文可以帮助你根据Keycloak决策来构建更多功能。例如,可以使用它构建一个仅展示已授权资源的动态菜单:

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。

现在假设我们项目的安全性需求发生了变化:除项目经理之外,PMOs现在也可以新建项目。

如果使用了Keycloak,处理这种新的需求时就完全不需要更改应用程序。一旦应用程序基于资源和范围来构建,只需更改授权服务器中特定资源关联的权限或策略。具体到上面的例子,只需要更改与项目资源和/或范围 urn:project.com:project:create 关联的权限和策略。

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

AuthorizationContext 还可用于获取你项目中配置的授权客户端API的引用:

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

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

JavaScript集成

Keycloak服务器附带一个JavaScript库,可以使用它与受策略执行器保护的资源服务器进行交互。这个库基于Keycloak JavaScript适配器,你的客户端可以直接集成它来从Keycloak服务器获得权限。

可以从Keycloak服务实例上在线获取它:

<script src="http://.../auth/js/keycloak-authz.js"></script>

然后可以方便地创建KeycloakAuthorization实例:

var keycloak = ... // obtain a Keycloak instance from keycloak.js library
var authorization = new KeycloakAuthorization(keycloak);

keycloak-authz.js 库主要提供了两个功能:

  • 如果资源服务器开启了UMA,则可以使用权限ticket来从Keycloak获得权限。
  • 发送请求的资源和范围,申请从服务器获得权限。

在这两种情况下,js库都允许你方便的和资源服务器或者Keycloak进行交互以获取token,然后客户端可以将其作为bearer token来访问资源服务器上受保护的资源。

处理来自UMA保护的资源服务器的授权响应

如果资源服务器由策略执行器保护,就将根据bearer token上携带的权限来做出响应。当携带的bearer token无权访问请求的资源时,服务器将响应401状态码及WWW-Authenticate头。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: UMA realm="${realm}",
    as_uri="https://${host}:${post}/auth/realms/${realm}",
    ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de"

到UMA授权流程查看更多细节。

客户端可以从资源服务器返回的WWW-Authenticate头中提取权ticket,并使js库来发送如下授权请求如下:

// prepare a authorization request with the permission ticket
var authorizationRequest = {};
authorizationRequest.ticket = ticket;

// send the authorization request, if successful retry the request
Identity.authorization.authorize(authorizationRequest).then(function (rpt) {
    // onGrant
}, function () {
    // onDeny
}, function () {
    // onError
});

authorize函数是完全异步的,使用回调来接收服务器响应:

  • onGrant:函数的第一个参数。如果授权成功,回调函数将接收到服务器返回的RPT。
  • onDeny:函数的第二个参数。仅当服务器拒绝授权请求时调用。
  • onError:第三个参数。仅当出现异常时调用。

大多数应用程序应该使用onGrant回调函数在401响应之后重试请求。后续请求应该用此RPT作为重试的bearer token。

获取权限

keycloak-authz.js 库提供了一个 entitlement 函数,用于从服务器获得RPT。

下面的例子展示了如何获取用户可以访问的全部资源和范围:

authorization.entitlement('my-resource-server-id').then(function (rpt) {
    // onGrant callback function.
    // If authorization was successful you'll receive an RPT
    // with the necessary permissions to access the resource server
});

下面的例子展示了如何获得具有特定资源和范围权限的RPT:

authorization.entitlement('my-resource-server', {
    "permissions": [
        {
            "id" : "Some Resource"
        }
    ]
}).then(function (rpt) {
    // onGrant
});

在使用 entitlement 函数时,必须提供要访问的资源服务器的client_id。

授权函数是完全异步的,支持一些回调函数来接收来自服务器的通知:

  • onGrant:函数的第一个参数。如果授权成功,回调函数将接收到服务器返回的RPT。
  • onDeny:函数的第二个参数。仅当服务器拒绝授权请求时调用。
  • onError:第三个参数。仅当出现异常时调用。

授权请求

authorize 和 authorize 函数都接受一个授权请求对象。该对象可以通过以下属性设置:

  • permissions

表示资源和范围的对象数组。例如:

var authorizationRequest = {
   "permissions": [
       {
           "id" : "Some Resource",
           "scopes" : ["view", "edit"]
       }
   ]
}
  •  metadata

定义服务器应如何处理授权请求。

        - response_include_resource_name

布尔值,指示服务器资源名是否应该包含在RPT的权限中。如果为false,则只包含资源标识符。

        - response_permissions_limit

整数N,它定义了RPT可以携带的权限数量上限。当与rpt参数一起使用时,只有最后N个请求的权限将保留在rpt中。

  • submit_request

布尔值,指示服务器是否需要创建对权限ticket的权限请求。此参数只有在与ticket参数一起,且在UMA授权流程时才会生效。

获取RPT

如果已经使用库提供的任何授权函数获得了RPT,则始终可以从授权对象获得RPT,如下所示:

var rpt = authorization.rpt;

设置TLS/HTTPS

如果server使用的是HTTPS,请在adapter中加入以下配置,来让授权客户端使用TLS/HTTPS协议访问Keycloak:

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

强烈建议通过HTTPS访问Keycloak。

 

源文档更新于2019-03-13。

转载于:https://my.oschina.net/landas/blog/3036222

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Keycloak授权服务指南 的相关文章

随机推荐