Keycloak SPI
Keycloak提供了强大的插件功能,这些插件通过SPI(Service Provider Interface)的方式集成到keycloak中。
SPI是扩展系统的一种通用方案,它相当于一个接口规范,要求实现者必须按照接口定义来实现方法,而系统里面则会调用SPI的方法来实现系统功能。
Keycloak提供了众多的SPI,供用户根据自己业务需要进行扩展和实现。比如:
- ThemeSelectorProviderFactory,用于实现自定义的主体选择器
- LocaleSelectorProviderFactory,用于实现自定义的国际化选择器
- RealmResourceProviderFactory,用于实现自定义的REST接口
- JpaEntityProviderFactory,用于扩展JPA的entity
除此之外,keycloak还允许定义自己的SPI。这些自定义SPI通常可以被REST的自定义Provider配合使用,来达到一定的业务目的。
本文的重点,就是演示如何自定义SPI,如何让keycloak使用它,以及如何通过这个SPI来扩展keycloak的功能。
自定义SPI示例
定义SPI的接口
首先,需要定义ProviderFacotry和Provider的子接口,表示你的SPI需要实现的功能。
1)定义Provider接口
这个接口里面的方法,就是你要暴露给使用者的功能。
java
package com.dadaer.keycloak.example.spi;
import org.keycloak.provider.Provider;
public interface ExampleServiceProvider extends Provider {
// 定义你的SPI需要暴露的功能,这里使用sayHi举例
String sayHi(String name);
}
2)定义ProviderFacotry接口
java
package com.dadaer.keycloak.example.spi;
import org.keycloak.provider.ProviderFactory;
public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleServiceProvider> {
}
声明SPI接口
定义完了SPI接口,需要通知Keycloak,让它知道这些SPI。因此需要编写一个实现了org.keycloak.provider.Spi
的类,如下:
java
package com.dadaer.keycloak.example.spi;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ExampleSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
// 定义SPI的名字为example
return "example";
}
@Override
public Class<? extends Provider> getProviderClass() {
// 声明Provider接口是ExampleServiceProvider
return ExampleServiceProvider.class;
}
@Override
public Class<? extends ProviderFactory<ExampleServiceProvider>> getProviderFactoryClass() {
// 声明ProviderFactory接口是ExampleServiceProviderFactory
return ExampleServiceProviderFactory.class;
}
}
同样需要通知keycloak这个实现类,方式是在META-INF/services目录下增加一个叫做org.keycloak.provider.Spi
的文件,然后写入如下内容:
com.dadaer.keycloak.example.spi.ExampleSpi
使用SPI
在声明了自定义的SPI接口以后,我们需要使用它。这里通过扩展REST API的方式来使用(这种方式也是常见的使用自定义SPI的方式)。
1)定义一个RealmResourceProvider
实现类
java
package com.dadaer.keycloak.example.rest;
import com.dadaer.keycloak.example.spi.ExampleServiceProvider;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.services.resource.RealmResourceProvider;
public class MyRestProvider implements RealmResourceProvider {
private final KeycloakSession session;
public MyRestProvider(KeycloakSession session) {
this.session = session;
}
@Override
public Object getResource() {
return this;
}
@Override
public void close() {
}
@GET
@Path("")
@Produces(MediaType.TEXT_PLAIN)
public String sayHi(@QueryParam("name") String name) {
// 这里通过KeycloakSession来获取SPI的实例,然后调用其上的方法
ExampleServiceProvider example = session.getProvider(ExampleServiceProvider.class);
return example.sayHi(name);
}
}
这里的重点是,通过KeycloakSession#getProvider
方法来获取我们自定义好的SPI,然后调用它上面的方法。
2)定义一个RealmResourceProviderFactory
的实现
java
package com.dadaer.keycloak.example.rest;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory;
public class MyRestProviderFactory implements RealmResourceProviderFactory {
private final static String PROVIDER_ID = "example-rest";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public RealmResourceProvider create(KeycloakSession session) {
return new MyRestProvider(session);
}
// 省略其他方法...
}
3)通知keycloak这个SPI实现 在META-INF/services目录下,新建一个叫做org.keycloak.services.resource.RealmResourceProviderFactory
的文件,内容如下:
com.dadaer.keycloak.example.rest.MyRestProviderFactory
实现SPI
在定义和声明完自定义SPI以后,我们可以去实现它。
1)实现MyExampleServiceProvider
java
package com.dadaer.keycloak.example.impl;
import com.dadaer.keycloak.example.spi.ExampleServiceProvider;
import org.keycloak.models.KeycloakSession;
public class MyExampleServiceProvider implements ExampleServiceProvider {
private final KeycloakSession session;
public MyExampleServiceProvider(KeycloakSession session) {
this.session = session;
}
@Override
public String sayHi(String name) {
return "Hello, " + name + " from " + session.getContext().getRealm().getName();
}
@Override
public void close() {
}
}
这里,我们实现了SPI中定义的sayHi
方法。
2)实现ExampleServiceProviderFactory
接口
java
package com.dadaer.keycloak.example.impl;
import com.dadaer.keycloak.example.spi.ExampleServiceProvider;
import com.dadaer.keycloak.example.spi.ExampleServiceProviderFactory;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class MyExampleServiceProviderFactory implements ExampleServiceProviderFactory {
private static final String PROVIDER_ID = "my-spi-example";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public ExampleServiceProvider create(KeycloakSession keycloakSession) {
return new MyExampleServiceProvider(keycloakSession);
}
// 省略其他方法...
}
可以看到,这里最主要实现的方法是getId
和create
,分别返回唯一id和具体的Provider实现类。
3)通知keycloak这个SPI实现 在META-INF/services目录下,新增一个叫做com.dadaer.keycloak.example.impl.MyExampleServiceProviderFactory
的文件,内容如下:
rust
com.dadaer.keycloak.example.impl.MyExampleServiceProviderFactory
测试
在把上面的步骤执行完以后,打包生成一个jar文件,放到keycloak的providers目录,然后重启keycloak。
bash
# 打包
mvn clean package
# 把生成的jar文件拷贝到keycloak的providers目录
cp -f target/custom-spi.jar $KEYCLOAK_HOME/providers
# 重启keycloak
cd $KEYCLOAK_HOME
./bin/kc.sh start-dev --http-port=8180
通过下面的命令来测试结果:
bash
curl http://localhost:8180/realms/master/example-rest?name=Jimmy
总结
Keycloak支持以SPI的方式来扩展功能,甚至可以让用户自定义SPI。自定义SPI的大致步骤为:
- 定义SPI接口,指定提供的功能和方法
- 通过
org.keycloak.provider.Spi
来声明自定义SPI接口 - 在Keycloak中使用SPI接口定义的方法
- 实现自定义的SPI接口