使用keycloak自定义SPI接入外部用户登录

发布于:2022-12-08 ⋅ 阅读:(1725) ⋅ 点赞:(1)

转载于:https://www.ycblog.top/article?articleId=399&pageNum=1

Keycloak 是一个针对现代应用程序和服务的开源身份和访问管理解决方案。Keycloak 支持单点登录(Single-Sign On),因此服务可以通过 OpenID Connect、OAuth 2.0 等协议对接 Keycloak。同时 Keycloak 也支持集成不同的身份认证服务,例如 Github、Google 和 Facebook 等。对于keycloak其他说明可查看官方文档:https://www.keycloak.org/

1、准备用户表,这些字段是必须的,不过字段名可以自定义,会在自定义的SPI中固定字段名,查询数据库的字段需要映射到实体的字段
在这里插入图片描述

2、添加pom依赖,打包需要同时把引入的依赖一起打包,下面是整个pom文件

<licenses>
    <license>
        <name>Apache License, Version 2.0</name>
        <url>http://www.apache.org/licenses/LICENSE-2.0</url>
        <distribution>repo</distribution>
    </license>
</licenses>

<repositories>
    <repository>
        <id>jboss</id>
        <url>https://repository.jboss.org/nexus/content/groups/public/</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

<properties>
    <keycloak.version>16.1.1</keycloak.version>
    <version.compiler.maven.plugin>3.5.1</version.compiler.maven.plugin>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-core</artifactId>
        <version>${keycloak.version}</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-server-spi</artifactId>
        <version>${keycloak.version}</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.jboss.logging</groupId>
        <artifactId>jboss-logging</artifactId>
        <version>3.4.3.Final</version>
        <scope>provided</scope>
    </dependency>

    <!-- 连接池 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>4.0.3</version>
    </dependency>

    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.15</version>
    </dependency>

    <!-- JDBC 工具包 -->
    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.7</version>
    </dependency>
</dependencies>

<build>
    <finalName>bpm-auth</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifest>
                        <mainClass/>
                    </manifest>
                </archive>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

3、自定义用户表实体 BaseUserEntity(不一定和数据库用户表进行对应)

public class BaseUserEntity {

    private String id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private String nickname;
    //租户id,可能存在租户隔离private String tenantId;

    .... get set 方法
}

4、创建用户字段适配器 BaseUserAdapter,可以添加自定义的属性字段,有时候我们需要传递一些额外的字段到keycloak中,可以在这里进行处理转换

public class BaseUserAdapter extends AbstractUserAdapterFederatedStorage {

    private static final Logger logger = Logger.getLogger(BaseUserAdapter.class);
    protected BaseUserEntity entity;
    protected String keycloakId;

    public BaseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, BaseUserEntity entity) {
        super(session, realm, model);
        this.entity = entity;
        keycloakId = StorageId.keycloakId(model, entity.getId());
    }

    public void setPassword(String password) {
        entity.setPassword(password);
    }
    
    public String getPassword() {
        return entity.getPassword();
    }

    @Override
    public String getUsername() {
        return entity.getUsername();
    }

    @Override
    public void setUsername(String username) {
        entity.setUsername(username);
    }

    @Override
    public void setEmail(String email) {
        entity.setEmail(email);
    }

    @Override
    public String getEmail() {
        return entity.getEmail();
    }

    @Override
    public String getId() {
        return keycloakId;
    }

    @Override
    protected boolean appendDefaultRolesToRoleMappings() {
        return false;
    }

    @Override
    public String getFirstName() {
        return entity.getNickname();
    }

    @Override
    public void setFirstName(String firstName) {
        entity.setNickname(firstName);
    }

    @Override
    public String getLastName() {
        return entity.getNickname();
    }

    @Override
    public void setLastName(String lastName) {
        entity.setNickname(lastName);
    }

    @Override
    protected Set<RoleModel> getRoleMappingsInternal() {
        HashSet<RoleModel> roleModels = new HashSet<>();
        logger.infof("============getRoleMappingsInternal=============");
        if(entity.getRoleId() != null && !entity.getRoleId().trim().isEmpty()){
            String roleName = entity.getRoleId() + "::" + entity.getRoleName();
            RoleModel role = realm.getRole(roleName);
            if (role == null) {
                role = realm.addRole(roleName);
            }
            roleModels.add(role);
        }
        return roleModels;
    }

    @Override
    public void setSingleAttribute(String name, String value) {
        logger.infof("设置单个属性:%s", name);
        if ("tenantId".equals(name)) {
            entity.setTenantId(value);
        } else {
            super.setSingleAttribute(name, value);
        }
    }

    @Override
    public void removeAttribute(String name) {
        if ("tenantId".equals(name)) {
            entity.setTenantId(null);
        } else {
            super.removeAttribute(name);
        }
    }

    @Override
    public void setAttribute(String name, List<String> values) {
        logger.infof("设置属性:%s", name);
        if ("tenantId".equals(name)) {
            entity.setTenantId(name);
        } else {
            super.setAttribute(name, values);
        }
    }

    @Override
    public String getFirstAttribute(String name) {
        if ("tenantId".equals(name)) {
            return entity.getTenantId();
        } else if ("firstName".equals(name)) {
            return entity.getNickname();
        } else {
            return super.getFirstAttribute(name);
        }
    }

    @Override
    public Map<String, List<String>> getAttributes() {
        Map<String, List<String>> attrs = super.getAttributes();
        MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();
        all.putAll(attrs);
        all.add("tenantId", entity.getTenantId());
        all.add("firstName", entity.getNickname());
        return all;
    }

    @Override
    public List<String> getAttribute(String name) {
        logger.infof("获取属性:%s, 值 %s", name, super.getAttribute(name));
        if ("tenantId".equals(name)) {
            List<String> tenantId = new LinkedList<>();
            tenantId.add(entity.getTenantId());
            return tenantId;
        } else if ("firstName".equals(name)) {
            List<String> firstName = new LinkedList<>();
            firstName.add(entity.getNickname());
            return firstName;
        } else {
            return super.getAttribute(name);
        }
    }
}

5、创建用户提供器BaseUserStorageProvider

该类实现了以下几个接口,这几个接口的含义如下:

1、UserStorageProvider, 自定义用户必须要实现的接口

2、UserLookupProvider, 实现后,可以根据userId/username从你自己的数据中查询用户数据

3、CredentialInputValidator, 实现后,可以更新密码

4、UserRegistrationProvider, 实现后,可以往自己数据库中增加删除修改用户数据

5、UserQueryProvider 实现后,可以从自己数据库中查询用户

以上几个接口的具体详细介绍可以到官网上查看,我们这里就简单说下本次用到的。

继承以上这些接口后,我们需要一步步具体的实现这些接口

/**
 * keycloak用户连接器
 * UserStorageProvider 自定义的StorageProvider必须要实现的接口
 * UserLookupProvider  实现后,可以根据userId/username从你自己的数据中查询用户数据
 * UserRegistrationProvider    实现后,可以往自己数据库中增加删除修改用户数据
 * CredentialInputUpdater  实现后,可以更新密码
 * CredentialInputValidator    验证密码的逻辑
 * UserQueryProvider   从自己数据库中查询用户
 *
 * @author yancao
 */
public class BaseUserStorageProvider implementsUserStorageProvider,
        UserLookupProvider,
        CredentialInputValidator,
        UserQueryProvider {

    private static final Logger logger = Logger.getLogger(BaseUserStorageProvider.class);

    protected ComponentModel model;
    protected KeycloakSession session;
    private final BaseUserStorageRepository userStorageDao;

    public BaseUserStorageProvider(ComponentModel model,
                                   KeycloakSession session,
                                   String tableName,
                                   String tableField,
                                   String userNameField,
                                   String emailField) {
        this.model = model;
        this.session = session;
        this.userStorageDao = new BaseUserStorageRepository(model, tableName, tableField, userNameField, emailField);
    }

    @Override
    public boolean supportsCredentialType(String credentialType) {
        logger.infof("=============> supportsCredentialType# %s", credentialType);
        return CredentialModel.PASSWORD.equals(credentialType);
    }

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        logger.infof("=============> isConfiguredFor# %s", credentialType);
        return supportsCredentialType(credentialType) && getPassword(user) != null;
    }

    /**
     * 验证用户是否正确
     *
     * @param realm realm
     * @param user  user
     * @param input input
     * @return boolean
     */
    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        logger.infof("=============> isValid");
        if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) {
            return false;
        }
        UserCredentialModel cred = (UserCredentialModel) input;
        logger.infof("=============> inputPassWord# %s", cred.getValue());
        logger.infof("=============> inputUserName# %s", user.getUsername());

        //模拟实现shiro sha加密,我的用户系统原来使用的是shiro,这里你根据自己的情况进行调整既可//String shaPwd = new FakeShiroSHAUtil(inputPwd, userExt.getSalt()).toString();

        String password = getPassword(user);
        logger.infof("=============> realPassWord# %s", password);

        return password != null && password.equals(cred.getValue());
    }

    /**
     * 获取密码
     *
     * @param user user
     * @return String
     */
    public String getPassword(UserModel user) {
        String password;
        if (user instanceof BaseUserAdapter) {
            //本地存储获取
            password = ((BaseUserAdapter) user).getPassword();
        } else {
            //从外部数据库查询出BaseUserEntity userExt = userStorageDao.getByUserName(user.getUsername());
            logger.infof(userExt.toString());
            password = userExt.getPassword();
        }
        return password;
    }

    @Override
    public void close() {
    }

    /**
     * 通过用户ID查询用户
     *
     * @param s          s
     * @param realmModel realmModel
     * @return UserModel
     */
    @Override
    public UserModel getUserById(String s, RealmModel realmModel) {
        logger.info("==============> getUserById:" + s);
        //注意:要用此方法获取到外部ID,直接传过来的ID是keycloak二次处理过的,需要处理为实际的idString externalId = StorageId.externalId(s);
        logger.infof(" 外部ID:id->%s,用户id:user_id->%s", s, externalId);
        //从数据库查询BaseUserEntity baseUser = userStorageDao.getById(externalId);if (null == baseUser) {
            logger.info("根据用户id为找到用户信息: " + externalId);
            return null;
        }
        return new BaseUserAdapter(session, realmModel, model, baseUser);
    }

    @Override
    public UserModel getUserByUsername(String username, RealmModel realmModel) {
        logger.info("==============> getUserByUsername: " + username);
        //从数据库查询BaseUserEntity baseUser = userStorageDao.getByUserName(username);if (null == baseUser) {
            logger.info("根据名称未找到用户信息: " + username);
            return null;
        }
        return new BaseUserAdapter(session, realmModel, model, baseUser);
    }

    @Override
    public UserModel getUserByEmail(String email, RealmModel realmModel) {
        logger.info("==============> getUserByEmail: " + email);
        //从数据库查询BaseUserEntity baseUser = userStorageDao.getByEmail(email);if (null == baseUser) {
            logger.info("could not find username: " + email);
            return null;
        }
        return new BaseUserAdapter(session, realmModel, model, baseUser);
    }

    /**
     * 查询用户总数
     *
     * @return int
     */
    @Override
    public int getUsersCount(RealmModel realm) {
        //从数据库查询
        logger.info("#################getUserCount############");
        return userStorageDao.count();
    }

    /**
     * 查询用户列表
     */
    @Override
    public List<UserModel> getUsers(RealmModel realmModel) {
        logger.error("#################getUsers");
        List<BaseUserEntity> baseUserList = userStorageDao.getUserList();
        return BaseUserStorageRepository.baseUserToUserModel(baseUserList, session, realmModel, model);
    }

    /**
     * 分页查询用户列表
     */
    @Override
    public List<UserModel> getUsers(RealmModel realmModel, int firstResult, int maxResults) {
        logger.error("#################getUsers-page");
        List<BaseUserEntity> baseUserList = userStorageDao.getUserList(firstResult, maxResults);
        return BaseUserStorageRepository.baseUserToUserModel(baseUserList, session, realmModel, model);
    }

    /**
     * 根据搜索框查询用户,搜索框默认模糊查询用户名和email
     */
    @Override
    public List<UserModel> searchForUser(String s, RealmModel realmModel) {
        logger.info("#################searchForUser");
        List<BaseUserEntity> baseUserList = userStorageDao.getUserBySearch(s);
        return BaseUserStorageRepository.baseUserToUserModel(baseUserList, session, realmModel, model);
    }

    /**
     * 分页搜索用户,搜索框默认模糊查询用户名和email
     *
     * @param s           s
     * @param realmModel  realmModel
     * @param firstResult firstResult
     * @param maxResults  maxResults
     * @return List
     */
    @Override
    public List<UserModel> searchForUser(String s, RealmModel realmModel, int firstResult, int maxResults) {
        logger.infof("query all user from database page, %d, %d", firstResult, maxResults);
        List<BaseUserEntity> baseUserList = userStorageDao.getUserBySearch(s, firstResult, maxResults);
        return BaseUserStorageRepository.baseUserToUserModel(baseUserList, session, realmModel, model);
    }

    /**
     * 根据查询条件查询用户
     */
    @Override
    public List<UserModel> searchForUser(Map<String, String> map, RealmModel realmModel) {
        logger.infof("#################searchForUser2");
        return searchForUser(map, realmModel, 0, 2000);

    }

    /**
     * 用户分页查询,在后台控制台的查询中会调用此方法
     *
     * @param map         map
     * @param realmModel  realmModel
     * @param firstResult firstResult
     * @param maxResults  maxResults
     * @return List
     */
    @Override
    public List<UserModel> searchForUser(Map<String, String> map, RealmModel realmModel, int firstResult, int maxResults) {
        logger.infof("this params map is unuseful !!! query all user from database page, %d, %d", firstResult, maxResults);
        List<BaseUserEntity> baseUserList = userStorageDao.getUserList(firstResult, maxResults);
        return BaseUserStorageRepository.baseUserToUserModel(baseUserList, session, realmModel, model);
    }

    @Override
    public List<UserModel> getGroupMembers(RealmModel realmModel, GroupModel groupModel) {
        return null;
    }

    @Override
    public List<UserModel> getGroupMembers(RealmModel realmModel, GroupModel groupModel, int i, int i1) {
        return null;
    }

    @Override
    public List<UserModel> searchForUserByUserAttribute(String s, String s1, RealmModel realmModel) {
        return null;
    }

}

6、创建提供者工厂,在keycloak管理界面添加用户联盟时的自定义的配置信息,主要是数据库连接

public class BaseUserStorageProviderFactory implements UserStorageProviderFactory<BaseUserStorageProvider> {

    private static final Logger logger = Logger.getLogger(BaseUserStorageProviderFactory.class);

    protected final List<ProviderConfigProperty> configMetadata;

    public BaseUserStorageProviderFactory() {
        logger.info("初始化用户提供器");

        //这些配置对应控制台的配置页面
        configMetadata = ProviderConfigurationBuilder.create()
                .property()
                .name(DbConnectionPool.CONFIG_KEY_JDBC_DRIVER)
                .label("JDBC驱动")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("com.mysql.cj.jdbc.Driver")
                .helpText("完整的JDBC驱动包名称")
                .add()
                .property()
                .name(DbConnectionPool.CONFIG_KEY_JDBC_URL)
                .label("JDBC URL")
                .type(ProviderConfigProperty.STRING_TYPE)
                .defaultValue("jdbc:mysql://")
                .helpText("JDBC URL连接串")
                .add()
                .property()
                .name(DbConnectionPool.CONFIG_KEY_DB_USERNAME)
                .label("账号")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("数据库用户名")
                .add()
                .property()
                .name(DbConnectionPool.CONFIG_KEY_DB_PASSWORD)
                .label("密码")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("数据库连接密码")
                .secret(true)
                .add()
                .property()
                .name(DbConnectionPool.TABLE_NAME)
                .label("用户表名")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("数据库表名,关联的用户表")
                .add()
                .property()
                .name(DbConnectionPool.SELECT_TABLE_FIELD)
                .label("查询字段")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("数据库查询字段,关联到接收数据字段,必须和实体中的字段对应,实体字段为:" +
                        "主键:id," +
                        "登录名:username," +
                        "密码:password," +
                        "邮箱:email," +
                        "电话:phone," +
                        "昵称:nickname," +
                        "租户:tenantId," +
                        "角色:roleId;" +
                        "例如:id as id,login_name as username,login_pwd as password,email as email,mobile as phone,role_id as roleId")
                .add()
                .property()
                .name(DbConnectionPool.USER_NAME_TABLE_FIELD)
                .label("用户名字段")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("用户名字段,用于登录根据用户名查询用户的字段名")
                .add()
                .property()
                .name(DbConnectionPool.EMAIL_TABLE_FIELD)
                .label("邮箱字段")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("邮箱字段,用于登录根据邮箱查询用户的字段名")
                .add()
                .build();
    }

    /**
     * 初始化
     *
     * @param config config
     */
    @Override
    public void init(Config.Scope config) {
    }

    @Override
    public BaseUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        try {
            logger.infof("创建用户提供器,开始加载数据库连接池... %s", model.getConfig().getFirst(DbConnectionPool.CONFIG_KEY_JDBC_URL));

            //初始化数据库连接
            DbConnectionPool.getInstance().initDb(model);

            return new BaseUserStorageProvider(model, session,
                    model.getConfig().getFirst(DbConnectionPool.TABLE_NAME),
                    model.getConfig().getFirst(DbConnectionPool.SELECT_TABLE_FIELD),
                    model.getConfig().getFirst(DbConnectionPool.USER_NAME_TABLE_FIELD),
                    model.getConfig().getFirst(DbConnectionPool.EMAIL_TABLE_FIELD),
                    model.getConfig().getFirst(DbConnectionPool.ROLE_TABLE_NAME));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 提供器名称,在控制台中选择的时候显示
     */
    @Override
    public String getId() {
        return "custom-user-provider";
    }

    @Override
    public String getHelpText() {
        return "自定义用户提供器";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }

    /**
     * 验证数据库连接是否正常
     *
     * @param session session
     * @param realm   realm
     * @param config  config
     * @throws ComponentValidationException ComponentValidationException
     */
    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
        System.out.println("#####################validateConfiguration#############" + config.getId());
        //初始化数据库连接
        MultivaluedHashMap<String, String> configMap = config.getConfig();
        DataSource dataSource = new HikariConf().init(configMap);

        try (Connection c = dataSource.getConnection()) {
            if (!c.isValid(20000)) {
                throw new ComponentValidationException("连接数据库超时");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new ComponentValidationException("无法校验数据库连接", e);
        }
    }

}

7、添加插件的配置

要让插件正常使用,我们还需要添加一个配置文件,在项目的resources路径下,新建

META-INF\services\org.keycloak.storage.UserStorageProviderFactory

在这里插入图片描述

在org.keycloak.storage.UserStorageProviderFactory文件中写入刚才创建的入口factory类的路径

比如我们的BaseUserStorageProviderFactory,带完整包名

com.wishare.auth.provider.BaseUserStorageProviderFactory

8、Dao层

public class BaseUserStorageRepository {

    private static final Logger logger = Logger.getLogger(BaseUserStorageRepository.class);

    private final ComponentModel model;

    private final String tableName;

    private final String selectTableField;

    private final String userNameField;

    private final String emailField;

    public BaseUserStorageRepository(ComponentModel model,
                                     String tableName,
                                     String selectTableField,
                                     String userNameField,
                                     String emailField) {
        this.model = model;
        this.tableName = tableName;
        this.selectTableField = selectTableField;
        this.userNameField = userNameField;
        this.emailField = emailField;
    }

    /**
     * 查询用户
     *
     * @param s s
     * @return BaseUserEntity
     */
    public BaseUserEntity getByUserName(String s) {
        logger.infof("根据用户名查询:%s", s);
        String getUserByUsername = "select "+selectTableField+" from " + tableName + " where "+userNameField+" = ?";
        return queryOne(getUserByUsername, s);
    }

    /**
     * 根据ID查询
     *
     * @param id id
     * @return BaseUserEntity
     */
    public BaseUserEntity getById(String id) {
        logger.infof("根据ID查询:%s", id);
        String getUserById = "select "+selectTableField+" from " + tableName + " where id = ?";
        return queryOne(getUserById, id);
    }

    /**
     * 查询总数
     *
     * @return int
     */
    public int count() {
        try (Connection ignored = DbConnectionPool.getInstance().getConnection(model.getId())) {
            String getUserCount = "select count(*) from " + tableName;
            Long count = DbConnectionPool.getInstance().getQueryRunner(model.getId()).query(getUserCount, new ScalarHandler<>());
            logger.infof("用户总数 : %d", count);
            return Math.toIntExact(count);
        } catch (SQLException e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 查询用户
     *
     * @param s s
     * @return BaseUserEntity
     */
    public BaseUserEntity getByEmail(String s) {
        logger.infof("根据邮箱查询:%s", s);
        String getUserByEmail = "select "+selectTableField+" from " + tableName + " where "+emailField+" = ?";
        return queryOne(getUserByEmail, s);
    }

    /**
     * 查询用户列表
     *
     * @return List
     */
    public List<BaseUserEntity> getUserList() {
        logger.infof("查询用户列表");
        String getAllUser = "select "+selectTableField+" from " + tableName;
        return queryList(getAllUser);
    }

    /**
     * 查询用户列表
     *
     * @return List
     */
    public List<BaseUserEntity> getUserList(int start, int offset) {
        logger.infof("分页查询用户");
        String getUserByPage = "select "+selectTableField+" from " + tableName + " limit ?,?";
        return queryList(getUserByPage, start, offset);
    }

    /**
     * 查询用户列表
     *
     * @return List
     */
    public List<BaseUserEntity> getUserBySearch(String key) {
        logger.infof("用户名或邮箱查询:%s", key);
        String getUserByUsernameOrEmail = "select "+selectTableField+" from " + tableName + " where "+userNameField+" = ? or "+emailField+" = ?";
        return queryList(getUserByUsernameOrEmail, key,key);
    }

    /**
     * 查询用户列表
     *
     * @return List
     */
    public List<BaseUserEntity> getUserBySearch(String key, int start, int offset) {
        logger.infof("用户名或邮箱分页查询:%s, %d, %d", key, start, offset);
        String getUserByUsernameOrEmailPage = "select "+selectTableField+" from " + tableName + " where "+userNameField+" = ? or "+emailField+" = ? limit ?,?";
        return queryList(getUserByUsernameOrEmailPage, key,key, start, offset);
    }

    /**
     * 查询
     *
     * @return BaseUserEntity
     */
    protected BaseUserEntity queryOne(String sql, Object... params) {
        try (Connection ignored = DbConnectionPool.getInstance().getConnection(model.getId())) {
            //开启驼峰映射BeanProcessor bean = new GenerousBeanProcessor();RowProcessor processor = new BasicRowProcessor(bean);//从数据库查询return DbConnectionPool.getInstance().getQueryRunner(model.getId()).query(sql, new BeanHandler<>(BaseUserEntity.class,processor), params);
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 查询列表
     *
     * @param sql    sql
     * @param params params
     * @return List
     */
    protected List<BaseUserEntity> queryList(String sql, Object... params) {
        try (Connection ignored = DbConnectionPool.getInstance().getConnection(model.getId())) {
            //开启驼峰映射BeanProcessor bean = new GenerousBeanProcessor();RowProcessor processor = new BasicRowProcessor(bean);//从数据库查询return DbConnectionPool.getInstance().getQueryRunner(model.getId()).query(sql, new BeanListHandler<>(BaseUserEntity.class,processor), params);
        } catch (SQLException e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }

    /**
     * 用户对象转keycloak对象
     *
     * @param baseUserList baseUserList
     * @param session      session
     * @param realmModel   realmModel
     * @param model        model
     * @return List
     */
    public static List<UserModel> baseUserToUserModel(List<BaseUserEntity> baseUserList, KeycloakSession session, RealmModel realmModel, ComponentModel model) {
        if (null == baseUserList || baseUserList.size() == 0) {
            return Collections.emptyList();
        }
        List<UserModel> allUser = new ArrayList<>();
        baseUserList.forEach(b ->
                allUser.add(new BaseUserAdapter(session, realmModel, model, b))
        );
        return allUser;
    }
}

9、数据库连接工具

/**
 * 统一数据库连接工具
 *
 * @author ChangSir
 */
public class DbConnectionPool {

    private static final Logger log = Logger.getLogger(DbConnectionPool.class);

    /**
     * jdbc
     */public static final String CONFIG_KEY_JDBC_DRIVER = "CONFIG_KEY_JDBC_DRIVER";
    public static final String CONFIG_KEY_JDBC_URL = "CONFIG_KEY_JDBC_URL";
    public static final String CONFIG_KEY_DB_USERNAME = "CONFIG_KEY_DB_USERNAME";
    public static final String CONFIG_KEY_DB_PASSWORD = "CONFIG_KEY_DB_PASSWORD";
    public static final String TABLE_NAME = "TABLE_NAME";
    public static final String SELECT_TABLE_FIELD = "SELECT_TABLE_FIELD";
    public static final String USER_NAME_TABLE_FIELD = "USER_NAME_TABLE_FIELD";
    public static final String EMAIL_TABLE_FIELD = "EMAIL_TABLE_FIELD";
    public static final String ROLE_TABLE_NAME = "ROLE_TABLE_NAME";
    /**
     * 数据源配置
     */private ConcurrentHashMap<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();

    private static class DbConnectionHolder {
        private static DbConnectionPool dbConnectionPool = new DbConnectionPool();
    }

    private DbConnectionPool() {
    }

    /**
     * 单例模式实现
     *
     * @return DBConnectionPool
     */
    public static synchronized DbConnectionPool getInstance() {
        return DbConnectionHolder.dbConnectionPool;
    }

    /**
     * 设置Db
     *
     * @param model model
     */
    public void initDb(ComponentModel model) {

        String dataId = model.getId();
        MultivaluedHashMap<String, String> configMap = model.getConfig();
        log.infof("init hikariCP pool... %s", dataId);

        DataSource dataSource = dataSourceMap.get(dataId);

        if (null == dataSource) {
            log.infof("创建datasource");
            dataSourceMap.put(dataId, new HikariConf().init(configMap));
        }
    }

    /**
     * 从连接池中获取链接
     *
     * @return Connection
     */
    public synchronized Connection getConnection(String id) {
        try {
            return dataSourceMap.get(id).getConnection();
        } catch (SQLException e) {
            log.debug("获取连接失败:" + e.getMessage());
            return null;
        }
    }

    /**
     * 获取query
     *
     * @return QueryRunner
     */
    public QueryRunner getQueryRunner(String id) {
        return new QueryRunner(dataSourceMap.get(id));
    }
}

10、连接池配置

/**
 * HikariCP数据库连接
 *
 * @author ChangSir
 */
public class HikariConf {

    private static final Logger log = Logger.getLogger(HikariConf.class);

    public DataSource init(MultivaluedHashMap<String, String> configMap) {

        log.infof("init HikariCP");

        HikariConfig config = new HikariConfig();
        config.setDriverClassName(configMap.getFirst(DbConnectionPool.CONFIG_KEY_JDBC_DRIVER));
        config.setJdbcUrl(configMap.getFirst(DbConnectionPool.CONFIG_KEY_JDBC_URL));
        config.setUsername(configMap.getFirst(DbConnectionPool.CONFIG_KEY_DB_USERNAME));
        config.setPassword(configMap.getFirst(DbConnectionPool.CONFIG_KEY_DB_PASSWORD));

        //是否自定义配置,为true时下面两个参数才生效
        config.addDataSourceProperty("cachePrepStmts", true);
        //连接池大小默认25,官方推荐250-500
        config.addDataSourceProperty("prepStmtCacheSize", 10);
        //单条语句最大长度默认256,官方推荐2048
        config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048);
        //新版本MySQL支持服务器端准备,开启能够得到显著性能提升
        config.addDataSourceProperty("useServerPrepStmts", true);
        config.addDataSourceProperty("useLocalSessionState", true);
        config.addDataSourceProperty("useLocalTransactionState", true);
        config.addDataSourceProperty("rewriteBatchedStatements", true);
        config.addDataSourceProperty("cacheResultSetMetadata", true);
        config.addDataSourceProperty("cacheServerConfiguration", true);
        config.addDataSourceProperty("elideSetAutoCommits", true);
        config.addDataSourceProperty("maintainTimeStats", false);
        config.setConnectionTestQuery("SELECT 1 FROM DUAL");
        //池中最大链接数量 一个4核,1块硬盘的服务器,连接数 = (4 * 2) + 1 = 9,凑个整数,10就可以了
        config.setMaximumPoolSize(4);
        //等待连接池分配连接的最大时长(毫秒)4s,超过这个时长还没可用的连接则发生SQLException, 缺省:30秒
        config.setConnectionTimeout(4000);
        //一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
        config.setIdleTimeout(60000);

        /*
         * 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,
         * 参考MySQL wait_timeout参数(show variables like '%timeout%';)
         */
        config.setMaxLifetime(300000);
        return new HikariDataSource(config);
    }

}

11、打包部署

现在回到IDEA中,将我们刚才创建的项目打包成jar文件,然后找打keycloak的程序路径,将jar文件放到对应文件夹下,运行keycloak程序
在这里插入图片描述

12、启动之后配置用户联盟

如果keycloak启动正常,打开控制台,点击左侧的 用户联合菜单,右侧的下拉列表中就能看到我们刚才添加的提供器,名字为 custom-user-provider
在这里插入图片描述

添加数据库配置,每一项都有说明,配置后进行保存,会校验数据库连接

在这里插入图片描述

查看效果,点击用户然后查看所有能够查看到数据库的用户,然后就能用数据库的用户进行登录了!!!

在这里插入图片描述

如果登录失败可以看控制台的日志,找到具体错误进行处理!!

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到