Keycloak中新增实体(Entity)

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

相关推荐
分享牛1 分钟前
Operaton入门到精通22-Operaton 2.0 升级指南:Spring Boot 4 核心变更详解
java·spring boot·后端
jinanmichael1 分钟前
SpringBoot 如何调用 WebService 接口
java·spring boot·后端
深蓝轨迹2 分钟前
吃透 Spring Boot dataSource与Starter
java·spring boot·笔记·后端
spring2997924 分钟前
springboot和springframework版本依赖关系
java·spring boot·后端
文公子WGZ14 分钟前
将java 21切换成java 25
java·开发语言
一直都在57214 分钟前
Java序列化和反序列化
java·开发语言
AI精钢19 分钟前
OpenLobster 的优势与劣势:一次面向 OpenClaw 用户的架构审视
java·微服务·架构·ai agent·mcp·openclaw·openlobster
MonkeyKing_sunyuhua24 分钟前
本地将镜像打包推送到阿里云的镜像服务器
java·服务器·阿里云
飞Link27 分钟前
Kafka~本地Python Kafka发送数据,服务端Kafka消费不到
java·分布式·kafka
喵喵蒻葉睦28 分钟前
力扣 hot100 滑动窗口最大值 单调双端队列 java 简单题解
java·数据结构·算法·leetcode·双端队列·滑动窗口·队列