目录
总览
策略执行点(Policy Enforcemnt Point / PEP)是一种设计模式,可以通过不同的方式实现这种设计。Keycloak为在不同的平台、环境以及编程语言下实现PEP提供了所有必须的方法。Keyclope授权服务提供了一个RESTful API,并利用OAuth2授权功能,使用集中授权服务器进行细粒度授权。
PEP通过keycloak服务,基于与受保护的资源相关联的策略做出访问决策。它在应用程序中充当过滤器或拦截器,以检查是否可以根据这些决定授予的权限满足对受保护资源的特定请求。
权限判断方法和服务使用的协议有关。如果使用UMA,则策略执行器需要通过bearer token获取RPT,并通过RPT决策资源是否可以被访问。所以客户端需要先从keycloak获取RPT,再发送给资源服务器。
但是,如果没有使用UMA,用户也可以使用常规的access token给资源服务器。这种情况下,策略执行器会尝试直接向keycloak请求权限。
如果你使用keycloak OIDC适配器,你可以轻易通过向如下字段添加策略执行器:
keycloak.json
{
"policy-enforcer": {}
}
当启用策略执行器时,发送给应用程序的所有请求都会被策略执行器拦截。策略执行器根据Keycloak授予的权限决策是否授予对受保护资源的访问。
策略执行器和应用路径以及使用Keycloak管理控制台创建的资源紧密关联。当你在keycloak创建资源服务器时,keycloak会创建默认配置,所以可以很快的使用策略执行器。
配置
要为应用启用策略执行器,需要在中keycloak.json中添加如下属性:
keycloak.json:
{
"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协议。如果配置此选项,则适配器会向服务查询权限票据,并根据UMA规范返回票据给客户端。如果没有配置,则策略执行器会基于常规的access token或RPT鉴权。这种话情况下,如果token中缺少权限,策略执行器会尝试直接向服务器获取权限。 - enforcement-mode
指明策略执行模式- ENFORCING
默认模式,即使请求的资源没有关联的鉴权策略,也会被拒绝 - PERMISSIVE
即使没有鉴权策略与请求的资源关联,也会授权访问 - DSIABLED
禁用鉴权策略,允许对任何资源的访问。应用依然可以通过及安全上下文向keycloak请求所有权限
- ENFORCING
- on-deny-redirecr-to
定义当查询到access denied信息时,客户端应该被重定向的URL。默认情况下适配器返回403响应状态码 - path-cache
定义策略执行器应如何跟踪应用程序中的路径与Keycloak中定义的资源之间的关联。通过缓存路径和受保护资源之间的关联,需要缓存来避免对keycloak服务的不必要请求。- lifespan
定义实体过期时间,以毫秒为单位。默认值是30000。设置为0时表明禁用缓存。设置为-1时禁用缓存过期时间。 - max-entires
定义缓存的最高实体数量,默认值是1000
- lifespan
- paths
指定被保护的路径。如果没有配置,那么策略执行器会通过Keycloak中注册的定义了URIS的应用找出所有所有的路径。- name
和路径关联的资源名称。当和path联合使用时,策略执行器会忽略资源的URIS属性 - path
必须,和应用上下文路径关联的URI。如果定义了这个选项,策略执行器会向服务查询URI相同和这个选项值相同的资源。目前支持如下路径匹配逻辑:- 通配符:/*
- 后缀:/*.html
- 子路径:/path/*
- 路径参数:/resource/{id}
- 精确匹配:/resource
- 按模式匹配:/{version}/resource,/api/{version}/resource,/api/{version}/resource/*
- methods
受保护的http请求方法以及与之相关的资源操作- method
http方法名称 - scopes
字符数组,表示和method相关的一组操作。当把操作和http方法关联的时候,客户端访问受保护资源时必须提供包含全部操作权限的RPT。比如,定义了POST方法和create关联,那么使用POST方法访问路径时必须携带含有create操作权限的RPT。 - scope-enforcement-mode
字符串,代表与操作相关的策略执行模式,可以是ALL或ANY。如果设置为ALL,那么使用设置的方法访问指定的路径时必须获得全部权限。如果设置为ANY,则需要至少满足一个权限。
- method
- enforcement-mode
定义策略如何执行。- ENFORCING
默认模式。如果请求访问的资源没有关联的策略,则拒绝访问。 - DISABLED
- ENFORCING
- claim-information-point
定义一组claim,这些claim必须被解析并发送给keycloak服务,这样策略才可以收到相关claim。
- name
- lazy-load-paths
指定适配器应如何从服务器获取与应用程序中的路径相关联的资源。这个配置项设置为true时,策略执行器会根据请求的路径向服务获取资源。当您不想在部署过程中从服务器获取所有资源(如果您没有提供路径)或只定义了一个子集的路径并希望按需获取其他资源时,此配置特别有用 - http-method-as-scope
指明请求的操作如何和http方法关联。如果设置为true,策略执行器会使用HTTP方法检查当前请求是否可以被授权。使用这个特性的时候,要确保在keycloak中注册的资源和要保护的http方法正确的关联。 - claim-information-point
定义一组必须解析并发送给keycloak的全局claim。这些声明会由策略执行器访问。
- user-managed-access
Claim Information Point
声明信息点负责解析声明并推送给keycloak,目的是给授权评估时有更丰富的访问上下文。这些声明可以作为策略执行器的配置项以获取不同来源的声明,比如:
- HTTP请求(参数、请求头、请求体等)
- 外部HTTP服务
- 配置中的静态值
- 通过SPI实现获取
当keycloak收到claim后,授权策略在决策时,除了基于发起请求的用户身份外,还可以基于请求上下文以及内容做出决策。这是为了使用上下文以及运行时信息做出细粒度的授权决策。
获取HTTP请求中的信息
下例展示了如何获取HTTP请求中的claim。
keycloak.json
"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']} "
}
}
}
]
}
从外部服务获取信息
下例展示如何从外部服务获取信息
"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"],
}
}
}
]
}
从SPI获取信息
如果内置的声明服务不能满足使用需求,开发者可以通过声明信息提供SPI支持不同的声明信息需求。
实现新的CIP提供服务,你需要实现org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory接口和ClaimInformationPointProvider接口,并在应用的classpath中提供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方法返回的name关联。name将用于映射配置中的claim-information-point。
处理请求时,策略执行器会调用MyClaimInformationPointProviderFactory.create方法以获取MyClaimInformationPointProvider实例。调用之后,此CIP的配置会以map返回。
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
KeycloakSecurityContext keycloakSecurityContext =
(KeycloakSecurityContext) request
.getAttribute(KeycloakSecurityContext.class.getName());
AuthorizationContext authzContext =
keycloakSecurityContext.getAuthorizationContext();
鉴权上下文可以用于细粒度控制服务的决策与返回。比如,你可以根据资源和操作的权限构建动态的菜单:
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授权服务的主要功能。上例可以看出,受保护资源不是直接和管理这些资源的策略关联的。
加入基于角色控制访问,则代码如下:
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、基于属性的访问控制(ABAC)还是任何其他访问控制变体,通过Keycloak,您都可以创建更易于管理的代码,直接关注您的资源。要么你拥有给定资源或操作的权限,要么你没有。
假设你的安全需求发生变化。如果使用keycloak,那么不需要更新代码。只要应用是基于资源和操作识别符处理逻辑,那么只要修改鉴权服务中与特定资源相关的权限和策略的配置即可。这时,与资源以及操作urn:project.com:project:create相关的权限和策略就会变化。
使用授权上下文获取授权客户端实例
AuthorizationContext也可以用于获取对授权客户端API的引用,该API为应用程序配置。
ClientAuthorizationContext clientContext = ClientAuthorizationContext.class.cast(authzContext);
AuthzClient authzClient = clientContext.getClient();
有时,被策略执行器保护的资源服务器需要访问授权服务器的API。使用AuhtzClient,资源服务器可以和鉴权服务交互以创建资源或检查特定的权限。
JavaScript integration
Keyclope服务器附带了一个JavaScript库,可以使用它与受策略执行器保护的资源服务器进行交互。这个库基于Keycloak Javascript适配器,集成后可以允许客户端从Keycloak服务器获取权限。
在运行的Keycloak Server实例中包含下面的script标签即可引用这个库:
<script src="http://.../js/keycloak-authz.js"></script>
应用后可按如下方法创建KeycloakAuthorization实例:
const keycloak = ... // obtain a Keycloak instance from keycloak.js library
const authorization = new KeycloakAuthorization(keycloak);
keycloak-authz.js提供两大主要功能:
- 访问UMA保护的资源服务器时,使用权限票据从服务器获取权限。
- 通过发送试图访问的资源和操作获取权限
在这两种情况下,通过JS库都可以轻松地与资源服务器和Keycloak授权服务进行交互,以获得客户端可以用作bearer token的,用于访问资源服务器上受保护资源的权限的令牌。
处理受UMA保护的资源服务器返回的授权响应
如果资源服务器受策略执行器保护,那么它会基于bearer token携带的权限响应客户端请求。如果客户端访问受保护资源时缺少bearer token,那么资源服务器会返回401状态码以及WWW-Authenticate响应头:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: UMA realm="${realm}",
as_uri="https://${host}:${port}/realms/${realm}",
ticket="016f84e8-f9b9-11e0-bd6f-0021cc6004de"
客户端需要从WWW-Authenticate响应头抽取权限票据,再按如下方式发送授权请求:
// prepare a authorization request with the permission ticket
const 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
});
授权是完全异步的,并且支持多种回调函数以接受服务器的通知:
onGrant:回调函数第一参数。如果授权成功并且服务返回授予所需权限的RPT,那么回调函数会收到RPT。onDeny:回调函数第二参数。服务拒绝授权时调用。onError:回调函数第三参数。服务返回异常响应时调用。
大部分应用需要在收到401响应后调用onGrant回调函数。后续的请求都需要携带RPT作为bearer token。
获取权限
keycloak-authz.js库提供了entitlement函数用于获取RPT。
下例展示如何获取携带用户可以访问的所有资源与操作的权限的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。
entitlement函数是完全异步的,并支持一下回调:
onGrant:回调函数第一参数。如果授权成功并且服务返回授予所需权限的RPT,那么回调函数会收到RPT。onDeny:回调函数第二参数。服务拒绝授权时调用。onError:回调函数第三参数。服务返回异常响应时调用。
对请求授权
authorize和entitlement函数都接受一个授权请求对象。该对象可以设置如下属性:
- permissions
代表资源与操作的数组。如:
const authorizationRequest = {
"permissions": [
{
"id" : "Some Resource",
"scopes" : ["view", "edit"]
}
]
}
- metadata
一个对象,其属性定义授权请求应该如何处理。- response_indluce_resource_name
布尔值,资源名称是否需要包含在RPT中。如果是false,只会返回资源标识符。 - response_permissions_limit
整数。定义RPT可以返回的权限数量上限。当和rpt参数一起使用时,只有最新的N个权限会携带在RPT中。
- response_indluce_resource_name
- submit_request
布尔值。服务器是否应创建对权限票据引用的资源和操作的权限请求。只有ticker参数作为UMA授权流程一部分时此参数才会生效。
获取RPT
如果你已经通过其他方法获取令RPT,你可以使用如下方法从授权对象中获取RPT:
const rpt = authorization.rpt;