Keycloak关于JPA的SPI
关于扩展Keycloak SPI的文章
我们可以通过SPI来向Keycloak添加JPA的Entity,来满足我们的业务需求。本示例根据官方的examples改编而来,目的是更加清晰地展示如何自定义JPA的Entity。
该示例中,定义了一个新的实体(Entity)- Company
,用来保存公司信息。为了把它加入到keycloak中,需要实现JpaEntityProvider
以及JpaEntityProviderFactory
这两个接口。
然后,定义了一个REST API(从RealmResourceProviderFactory
而来),来通过JPA的EntityManager
实现对Company
实体的CRUD。
扩展JPA实体示例
实现JPA的SPI
定义实体对象
实体对象是符合JPA规范的POJO,即:具有特定JPA注解的Java类。以下是本示例要用到的实体。
java
package com.dadaer.keycloak.jpa.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
@Entity
@Table(name = "EXAMPLE_COMPANY")
@NamedQueries({@NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId")})
public class Company {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false)
private String name;
@Column(name = "REALM_ID", nullable = false)
private String realmId;
// getters and setters...
}
实现JpaEntityProvider
接口
java
package com.dadaer.keycloak.jpa;
import com.dadaer.keycloak.jpa.entity.Company;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.models.KeycloakSession;
import java.util.Collections;
import java.util.List;
public class CompanyJapEntityProvider implements JpaEntityProvider {
private final KeycloakSession session;
public CompanyJapEntityProvider(KeycloakSession session) {
this.session = session;
}
/**
* 返回所有需要管理的实体(Entity),这里只有一个Company实体
*/
@Override
public List<Class<?>> getEntities() {
return Collections.singletonList(Company.class);
}
/**
* 指定Liquibase的变更记录文件的位置,这里表示直接放在了classpath下面
*/
@Override
public String getChangelogLocation() {
return "example-changelog.xml";
}
@Override
public String getFactoryId() {
return CompanyJpaEntityProviderFactory.PROVIDER_ID;
}
@Override
public void close() {
}
}
这里需要注意两个点:
- 通过
getEntities
方法指定需要操作的实体集合 - 通过
getChangelogLocation
方法指定Liquibase变更记录文件的问题
Keycloak使用Liquibase作为数据库版本控制的工具,所以需要提供一个Liquibase的changelog文件,通常这个文件是xml格式,但是也支持JSON和YAML等格式。(这里用的是xml格式)。
实现JpaEntityProviderFactory
接口
java
package com.dadaer.keycloak.jpa;
import org.keycloak.Config;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class CompanyJpaEntityProviderFactory implements JpaEntityProviderFactory {
final static String PROVIDER_ID = "company-jpa-provider";
@Override
public JpaEntityProvider create(KeycloakSession session) {
return new CompanyJapEntityProvider(session);
}
@Override
public String getId() {
return PROVIDER_ID;
}
// 其他方法...
}
增加Liquibase的changelog文件
在resources目录下,新增一个文件:example-changelog.xml
,内容如下:
xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="erik.mulder@docdatapayments.com" id="example-1.0">
<createTable tableName="EXAMPLE_COMPANY">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey
constraintName="PK_COMPANY"
tableName="EXAMPLE_COMPANY"
columnNames="ID"/>
</changeSet>
</databaseChangeLog>
通知Keycloak这个JPA SPI的实现
在resources/META-INF/services目录下,新建一个文件:org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
,内容如下:
com.dadaer.keycloak.jpa.CompanyJpaEntityProviderFactory
使用实体
在定义了Company
这个实体以后,就可以对其进行增删改查等操作。因为实体本质上是JPA的Entity,所以需要使用JPA的API(EntityManager
)来操作它。这里不对具体的调用做过多的详解,只把重要的代码分享出来供参考。
java
private EntityManager getEntityManager() {
return session.getProvider(JpaConnectionProvider.class).getEntityManager();
}
protected RealmModel getRealm() {
return session.getContext().getRealm();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompanyRepresentation addCompany(CompanyRepresentation companyRep) {
Company entity = new Company();
String id = companyRep.getId() == null ? KeycloakModelUtils.generateId() : companyRep.getId();
entity.setId(id);
entity.setName(companyRep.getName());
entity.setRealmId(getRealm().getId());
getEntityManager().persist(entity);
companyRep.setId(id);
return companyRep;
}
@GET
@Path("")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<CompanyRepresentation> getCompanies() {
List<Company> companyEntities = getEntityManager()
.createNamedQuery("findByRealm", Company.class)
.setParameter("realmId", getRealm().getId())
.getResultList();
List<CompanyRepresentation> result = new LinkedList<>();
for (Company entity : companyEntities) {
result.add(new CompanyRepresentation(entity));
}
return result;
}
这里需要重点看以下几点:
- 通过
KeycloakSession#getProvider
方法获取到JPA的SPI示例,然后调用它上面的getEntityManager
方法获取EntityManager
对象 - 通过
EntityManager
的API操作实体
结论
通过实现Keycloak的JPA的SPI,我们可以向Keycloak中增加实体对象,来满足业务需要。在实现的时候,需要注意的是,提供Liquibase的changelog文件。