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文件。