Keycloak授权介绍
Keycloak提供了一套基于策略的权限控制,主要的控制对象是被保护的资源,通常是一个或者多个URIs。
Keycloak的权限控制通过所谓的PEP(Policy Enforcement Point)即:策略执行点来操作。
Keycloak授权系统的实体对象
如上图所以,把所有授权实体分成了三个大的部分。
最上面的部分,我称之为资源定义,负责设置哪些资源(resource)的那些范围(scope)要被权限控制。资源通常是一个或者一组URI,比如/protected/*,而范围(scope)是一个很灵活的概念,既可以是对资源的CRUD,也可以是具有某一种特质的一组对象。通常,我们可以用scope来标识对资源的CRUD,比如:urn:demo:protected:view,urn:demo:protected:delete等等。
下面的部分,我称之为执行策略,负责设置执行权限的方式。比如,可以根据用户是否属于某个角色来检查权限,或者可以根据时间来检查权限等等,这里我列出了一些常用的执行策略,其实每个策略对应的一个权限模型:
- 基于角色的权限控制
- 基于用户的权限控制
- 基于组的权限控制(组可以有层级关系)
- 基于时间的权限控制
- 基于正则表达式的权限控制
中间的部分,我称之为权限关联,负责把执行策略与资源进行绑定。
Keycloak关于授权的设置
在Keycloak中,授权相关的配制,绝大部分都在具体的客户端设置里面。并且必须开启授权开关。
集成Keycloak授权功能
在Keycloak上对资源的权限进行配置以后,就可以通过编程的方式,来利用Keycloak来保护我们的微服务(也称资源服务器)了。
keycloak权限设置
既可以通过keycloak的admin console来设置权限,也可以通过Java客户端来设置权限。无论哪种方式,大致的过程如下:
- 定义resource和scope
- 定义policy
- 根据需要定义角色、组等等
- 定义permission来关联resource(scope)与policy
通过admin console配置
主要就是理解了资源、scope、policy和permission的概念后,在admin console中依次进行配置即可。
通过Java客户端配置
1)引入依赖
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>${keycloak.version}</version>
</dependency>
2)编写代码
// 初始化授权客户端对象
AuthzClient authzClient = AuthzClient.create();
// 获取pat对象
ProtectionResource protection = authzClient.protection();
// 创建资源(resource)和scope,这里创建的资源名称为test-res,scope为create-scope,
// URI为/admin/*,类型为test:api
ResourceRepresentation res = new ResourceRepresentation("test-res",
Set.of(new ScopeRepresentation("create-scope")),
Set.of("/admin/*"),
"test:api");
res.setOwnerManagedAccess(true);
ProtectedResource resClient = protection.resource();
ResourceRepresentation existingRes = resClient.findByName(res.getName());
if (existingRes != null) {
resClient.delete(existingRes.getId());
}
res = resClient.create(res);
String resId = res.getId();
// 为上述资源创建权限
// 注意:这里创建的权限,在admin console上不可见
UmaPermissionRepresentation umaPerm = new UmaPermissionRepresentation();
umaPerm.setRoles(Set.of("user_premium"));
umaPerm.setName("Only premium user can create resources");
umaPerm.setDescription("Allow access to premium users");
umaPerm.setScopes(Set.of("create-scope"));
UmaPermissionRepresentation createdUmaPerm = protection.policy(resId).create(umaPerm);
建议
- 建议通过admin console来管理资源/scope/policy/permission
- 可以使用Java来管理资源,但是不建议使用Java来管理policy和permission
使用keycloak设置的权限保护资源
在keycloak上配置了资源、策略、权限以后,可以在资源服务器上来通过PEP进行权限控制。以下是步骤:
配置keycloak的enforcer
新增一个json文件,放在resources目录下,内容大致如下:
{
"realm": "quickstart",
"auth-server-url": "http://localhost:8180",
"resource": "authz-servlet",
"credentials": {
"secret": "secret"
},
"http-method-as-scope": false
}
增加一个Filter来解析上述配置,并拦截用户请求
在Spring Security的FilterChain中,增加ServletPolicyEnforcerFilter来真正执行权限检查。
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);
return http.build();
}
private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() {
return new ServletPolicyEnforcerFilter(new ConfigurationResolver() {
@Override
public PolicyEnforcerConfig resolve(HttpRequest request) {
try {
return JsonSerialization.readValue(getClass().getResourceAsStream("/policy-enforcer.json"), PolicyEnforcerConfig.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@Bean
JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
return NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri()).build();
}
}
Bonus:利用AuthorizationContext判断权限
除了使用ServletPolicyEnforcerFilter来进行权限检查以外,我们还可以利用AuthorizationContext来手动判断当前用户是否对某项资源有访问权限。示例代码如下:
@GetMapping("/protected/premium")
public String premium(@AuthenticationPrincipal Jwt jwt, HttpServletRequest request) {
// 获取当前的AuthorizationContext对象
AuthorizationContext context = (AuthorizationContext) request.getAttribute(AuthorizationContext.class.getName());
// 调用context上的方法进行手动判断
if (context.hasPermission("Premium Resource", "GET")) {
// do something intersting...
}
return String.format("Hello, %s!", jwt.getClaimAsString("preferred_username"));
}
这种代码,可以用来构建动态菜单。比如,在用户登录以后,通过这种权限判断,来觉得哪些菜单显示/隐藏。 |
Bonus:利用AuthzClient来操纵资源
通过AuthorizationContext,我们可以获取到AuthzClient,通过它我们就可以创建资源或者进行个别的权限检查...
// 将AuthorizationContext强转成ClientAuthorizationContext
ClientAuthorizationContext clientContext = (ClientAuthorizationContext) context;
// 从clientContext中获取AuthzClient实例,然后调用它上面的方法
AuthzClient client = clientContext.getClient();
ResourceRepresentation resource = client.protection().resource().findByName("Premium Resource");
System.out.println(resource);
Bonus:从JS调用keycloak的API
在JS中,可以通过keycloak提供的keycloak-authz.js的API来进行权限判断。
首先,需要引入keycloak-authz.js文件
<script src="http://.../js/keycloak-authz.js"></script> |
然后调用其上的API在JS端进行权限检查和控制。比如:
// 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
});