(一)keycloak 部署运行及源码打包
(二)keycloak 配置运行
(三)keycloak 基于SpringBoot、Servlet的客户端开发
(四)keycloak 自定义用户(SPI)开发
(未完成)(五)keycloak 自定义主题
(未完成)(六)keycloak 添加登录验证码功能
(七)keycloak 设置客户端访问类型 bearer-only
(八)keycloak 设置客户端访问类型 confidential


文章目录

  • 前言
  • 1.现有用户表结构
  • 2.开发自定义用户插件
    • 2.1 添加依赖
    • 2.2 创建我们自定义用户表的实体 BaseUserEntity
    • 2.3 创建用户字段适配器 BaseUserAdapter
    • 2.4 创建用户查询、更新、修改类
    • 2.5 插件入口,用户配置 BaseUserStorageProviderFactory
    • 2.6 添加插件的配置
    • 2.7 打包部署
  • 3 配置插件
    • 3.1 配置
    • 3.2 自定义属性(不需要可跳过)

前言

keycloak是一套完整的开源认证授权管理解决方案,由红帽开发,提供了多种语言库,方便集成。本系列教程以使用为主,介绍keycloak的搭建,源码编译,以及部分功能的二次开发。

keycloak官网提供了详细的教程以及示例,可以参考官网示例进行编写开发。
官网地址 本系列教程基于官网最新版本18.0进行编写。

在使用keycloak时,我们大多数情况下需要接入自己的用户数据,本章节我们主要介绍keycloak连接自己现有的数据库


1.现有用户表结构

我们这里仅以作者现有的表结构来说明,你可以参考此部分内容改为你自己的表结构
表结构如下:

2.开发自定义用户插件

插件使用了hikaricp作为数据库连接池,Apache的commons-dbutils作为jdbc的连接工具,这两个包仅供参考,你可以按照你自己的喜好使用不同的工具包

首先,我们打开IDEA创建一个名为custom-user的项目

2.1 添加依赖

在创建的custom-user项目的pom文件中,添加必要的依赖:

<dependency><groupId>org.keycloak</groupId><artifactId>keycloak-core</artifactId><version>18.0.0</version>
</dependency><dependency><groupId>org.keycloak</groupId><artifactId>keycloak-server-spi</artifactId><version>18.0.0</version>
</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><!-- JDBC 工具包 -->
<dependency><groupId>commons-dbutils</groupId><artifactId>commons-dbutils</artifactId><version>1.7</version>
</dependency>

2.2 创建我们自定义用户表的实体 BaseUserEntity

我们先根据自己的表结构创建一个我们自己用户表对应的pojo类

public class BaseUserEntity {private String id;private String username;private String nickname;private String password;private String salt;private String email;private String mobile;private int status;private LocalDateTime createTime;.... get set 方法
}

2.3 创建用户字段适配器 BaseUserAdapter

创建好我们自己的用户数据字段后,需要编写一个适配器,用来把自定义的用户字段转换成keycloak能够识别的用户字段

我们创建一个BaseUserAdapter类,该类继承 AbstractUserAdapterFederatedStorage

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());logger.infof("初始化用户适配器%s", entity.toString());}public String getPassword() {return entity.getPassword();}public void setPassword(String password) {entity.setPassword(password);}@Overridepublic String getUsername() {return entity.getUsername();}@Overridepublic void setUsername(String username) {entity.setUsername(username);}@Overridepublic void setEmail(String email) {entity.setEmail(email);}@Overridepublic String getEmail() {return entity.getEmail();}@Overridepublic String getId() {return keycloakId;}@Overridepublic String getFirstName() {return entity.getNickname();}@Overridepublic void setFirstName(String firstName) {entity.setNickname(firstName);}@Overridepublic String getLastName() {return entity.getNickname();}@Overridepublic void setLastName(String lastName) {entity.setNickname(lastName);}@Overridepublic void setSingleAttribute(String name, String value) {logger.infof("设置单个属性:%s", name);if (name.equals("phone")) {entity.setMobile(value);} else if (name.equals("nickname")) {entity.setNickname(value);} else if (name.equals("email")) {entity.setEmail(value);} else {super.setSingleAttribute(name, value);}}@Overridepublic void removeAttribute(String name) {if (name.equals("phone")) {entity.setMobile(null);} else if (name.equals("nickname")) {entity.setNickname(null);} else if (name.equals("email")) {entity.setEmail(null);} else {super.removeAttribute(name);}}@Overridepublic void setAttribute(String name, List<String> values) {logger.infof("设置属性:%s", name);if (name.equals("phone")) {entity.setMobile(values.get(0));} else if (name.equals("nickname")) {entity.setNickname(values.get(0));} else if (name.equals("email")) {entity.setEmail(values.get(0));} else {super.setAttribute(name, values);}}@Overridepublic String getFirstAttribute(String name) {logger.infof("获取First属性:%s", name);if (name.equals("phone")) {return entity.getMobile();} else if (name.equals("nickname")) {return entity.getNickname();} else if (name.equals("email")) {return entity.getEmail();} else {return super.getFirstAttribute(name);}}@Overridepublic Map<String, List<String>> getAttributes() {Map<String, List<String>> attrs = super.getAttributes();MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();all.putAll(attrs);all.add("phone", entity.getMobile());all.add("nickname", entity.getNickname());all.add("name", entity.getNickname());all.add("email", entity.getEmail());all.forEach((k, v) -> {logger.infof("获取所有属性:%s, 值 %s", k, v.toString());});return all;}@Overridepublic List<String> getAttribute(String name) {logger.infof("获取属性:%s, 值 %s", name, super.getAttribute(name));if (name.equals("phone")) {List<String> phone = new LinkedList<>();phone.add(entity.getMobile());return phone;} else if (name.equals("nickname")) {List<String> nickname = new LinkedList<>();nickname.add(entity.getNickname());return nickname;} else if (name.equals("email")) {List<String> email = new LinkedList<>();email.add(entity.getEmail());return email;} else {return super.getAttribute(name);}}
}

注意:在适配器的Attribute方法中,我们自定义了3个字段,
phone
nickname
email
有时候我们需要传递一些额外的字段到keycloak中,可以在这里进行处理转换

2.4 创建用户查询、更新、修改类

这一步是最主要的一步,我们在custom-user项目中添加一个名为BaseUserStorageProvider的类,该类实现了以下几个接口,这几个接口的含义如下:

  • UserStorageProvider, 自定义用户必须要实现的接口
  • UserLookupProvider, 实现后,可以根据userId/username从你自己的数据中查询用户数据
  • CredentialInputValidator, 实现后,可以更新密码
  • UserRegistrationProvider, 实现后,可以往自己数据库中增加删除修改用户数据
  • UserQueryProvider 实现后,可以从自己数据库中查询用户

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

继承以上这些接口后,我们需要一步步具体的实现这些接口,下面将作者的整个代码贴上来,你可以根据自己的实际情况进行修改调整:

/*** @autor ChangSir* @description keycloak用户连接器* UserStorageProvider  自定义的StorageProvider必须要实现的接口* UserLookupProvider 实现后,可以根据userId/username从你自己的数据中查询用户数据* UserRegistrationProvider  实现后,可以往自己数据库中增加删除修改用户数据* CredentialInputUpdater  实现后,可以更新密码* CredentialInputValidator 验证密码的逻辑* UserQueryProvider  从自己数据库中查询用户* @date 2022/6/10 13:57*/
public class BaseUserStorageProvider implements UserStorageProvider, UserLookupProvider,CredentialInputValidator,UserRegistrationProvider,UserQueryProvider {private static final Logger logger = Logger.getLogger(BaseUserStorageProvider.class);protected ComponentModel model;protected KeycloakSession session;private BaseUserStorageDao userStorageDao;public BaseUserStorageProvider(ComponentModel model, KeycloakSession session, Properties properties) {this.model = model;this.session = session;this.userStorageDao = new BaseUserStorageDao(properties, model);}@Overridepublic boolean supportsCredentialType(String s) {return CredentialModel.PASSWORD.equals(s);}@Overridepublic boolean isConfiguredFor(RealmModel realmModel, UserModel userModel, String s) {return supportsCredentialType(s);}/*** 验证用户是否正确** @param realmModel* @param userModel* @param input* @return*/@Overridepublic boolean isValid(RealmModel realmModel, UserModel userModel, CredentialInput input) {logger.infof("updateCredential# %s, %s", userModel, input);if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) {return false;}//输入的密码String inputPwd = ((UserCredentialModel) input).getValue();String username = userModel.getUsername();//从数据库查询出BaseUserEntity userExt = userStorageDao.getByUserName(username);//模拟实现shiro sha加密,我的用户系统原来使用的是shiro,这里你根据自己的情况进行调整既可String shaPwd = new FakeShiroSHAUtil(inputPwd, userExt.getSalt()).toString();return shaPwd.equals(userExt.getPassword());}@Overridepublic void close() {}/*** 通过用户ID查询用户* @param s* @param realmModel* @return*/@Overridepublic UserModel getUserById(String s, RealmModel realmModel) {logger.error("#################getUserById");//注意:要用此方法获取到外部ID,直接传过来的ID是keycloak二次处理过的,需要处理为实际的idString externalId = StorageId.externalId(s);logger.infof("query user from database by user_id: %s, externalId: %s", s, externalId);//从数据库查询BaseUserEntity baseUser = userStorageDao.getById(externalId);if (null == baseUser)return null;return new BaseUserAdapter(session, realmModel, model, baseUser);}/*** 根据用户名查询用户* /@Overridepublic UserModel getUserByUsername(String s, RealmModel realmModel) {logger.infof("query user from database by username: %s", s);//从数据库查询BaseUserEntity baseUser = userStorageDao.getByUserName(s);if (null == baseUser)return null;return new BaseUserAdapter(session, realmModel, model, baseUser);}/*** 根据email查询用户* /@Overridepublic UserModel getUserByEmail(String s, RealmModel realmModel) {logger.error("#################getUserByEmail");//从数据库查询BaseUserEntity baseUser = userStorageDao.getByEmail(s);if (null == baseUser)return null;return new BaseUserAdapter(session, realmModel, model, baseUser);}/*** 查询用户总数*/@Overridepublic int getUsersCount(RealmModel realm) {//从数据库查询logger.error("#################getUserCount");int count = userStorageDao.count();return count;}/*** 查询用户列表*/@Overridepublic List<UserModel> getUsers(RealmModel realmModel) {logger.error("#################getUsers");List<BaseUserEntity> baseUserList = userStorageDao.getUserList();return BaseUserStorageDao.baseUser2UserModel(baseUserList, session, realmModel, model);}/*** 分页查询用户列表*/@Overridepublic List<UserModel> getUsers(RealmModel realmModel, int i, int i1) {logger.error("#################getUsers-page");List<BaseUserEntity> baseUserList = userStorageDao.getUserList(i, i1);return BaseUserStorageDao.baseUser2UserModel(baseUserList, session, realmModel, model);}/*** 根据搜索框查询用户,搜索框默认模糊查询用户名和email*/@Overridepublic List<UserModel> searchForUser(String s, RealmModel realmModel) {logger.error("#################searchForUser");List<BaseUserEntity> baseUserList = userStorageDao.getUserBySearch(s);return BaseUserStorageDao.baseUser2UserModel(baseUserList, session, realmModel, model);}/*** 分页搜索用户,搜索框默认模糊查询用户名和email* @param s* @param realmModel* @param i* @param i1* @return*/@Overridepublic List<UserModel> searchForUser(String s, RealmModel realmModel, int i, int i1) {logger.infof("query all user from database page, %d, %d", i * i1, i1);List<BaseUserEntity> baseUserList = userStorageDao.getUserBySearch(s, i, i1);return BaseUserStorageDao.baseUser2UserModel(baseUserList, session, realmModel, model);}/*** 根据查询条件查询用户,未实现此方法,实际使用中暂时未找到该方法在哪里调用*/@Overridepublic List<UserModel> searchForUser(Map<String, String> map, RealmModel realmModel) {logger.error("#################searchForUser2");return Collections.emptyList();}/*** 用户分页查询,在后台控制台的查询中会调用此方法* @param map* @param realmModel* @param i* @param i1* @return*/@Overridepublic List<UserModel> searchForUser(Map<String, String> map, RealmModel realmModel, int i, int i1) {logger.infof("this params map is unuseful !!! query all user from database page, %d, %d", i * i1, i1);List<BaseUserEntity> baseUserList = userStorageDao.getUserList(i, i1);return BaseUserStorageDao.baseUser2UserModel(baseUserList, session, realmModel, model);}@Overridepublic List<UserModel> getGroupMembers(RealmModel realmModel, GroupModel groupModel) {logger.error("#################getGroupMembers");return Collections.emptyList();}@Overridepublic List<UserModel> getGroupMembers(RealmModel realmModel, GroupModel groupModel, int i, int i1) {logger.error("#################getGroupMembers1");return Collections.emptyList();}@Overridepublic List<UserModel> searchForUserByUserAttribute(String s, String s1, RealmModel realmModel) {logger.error("#################searchForUserByUserAttribute");return Collections.emptyList();}@Overridepublic void preRemove(RealmModel realm) {logger.error("#################preRemove");}@Overridepublic void preRemove(RealmModel realm, GroupModel group) {logger.error("#################preRemove1");}@Overridepublic void preRemove(RealmModel realm, RoleModel role) {logger.error("#################preRemove2");}/*** 添加用户* @param realmModel* @return*/@Overridepublic UserModel addUser(RealmModel realmModel, String username) {logger.infof("#######add new user into external database; %s", username);BaseUserEntity userEntity = userStorageDao.saveUser(username);return new BaseUserAdapter(session, realmModel, model, userEntity);}/*** 删除用户* @param realmModel* @param userModel* @return*/@Overridepublic boolean removeUser(RealmModel realmModel, UserModel userModel) {String userId = StorageId.externalId(userModel.getId());return userStorageDao.deleteUser(userId);}
}

上面代码中的 BaseUserStorageDao 就是实现数据库查询的代码,没有什么特殊的,你只要实现每个SQL查询就可以了,我这里为了适配不同的数据库,将查询语句放到了配置文件中了,通过读取配置文件的sql语句来实现查询,这里贴一下我的DAO层代码

public class BaseUserStorageDao {private static final Logger logger = Logger.getLogger(BaseUserStorageDao.class);//配置文件protected Properties properties;private ComponentModel model;public BaseUserStorageDao(Properties properties, ComponentModel model) {this.model = model;this.properties = properties;}/*** 查询用户** @param s* @return*/public BaseUserEntity getByUserName(String s) {logger.infof("根据用户名查询%s", s);String sql = properties.getProperty("queryByUsername", UserQuerySQL.QUERY_BY_USERNAME);return queryOne(sql, s);}/*** 查询用户** @param s* @return*/public BaseUserEntity getByEmail(String s) {logger.infof("根据邮箱查询%s", s);String sql = properties.getProperty("queryByEmail", UserQuerySQL.QUERY_BY_EMAIL);return queryOne(sql, s);}/*** 根据ID查询** @param id* @return*/public BaseUserEntity getById(String id) {logger.infof("根据ID查询%s", id);String sql = properties.getProperty("queryById", UserQuerySQL.QUERY_BY_ID);return queryOne(sql, id);}/*** 查询总数** @return*/public int count() {try (Connection c = DBConnectionPool.getInstance().getConnection(model.getId())) {String sql = properties.getProperty("queryCount", UserQuerySQL.QUERY_COUNT);int count = DBConnectionPool.getInstance().q(model.getId()).query(sql, new ScalarHandler<>());logger.infof("用户总数 : %d", count);return count;} catch (SQLException e) {e.printStackTrace();return 0;}}/*** 查询用户列表** @return*/public List<BaseUserEntity> getUserList() {logger.infof("查询列表");String sql = properties.getProperty("queryList", UserQuerySQL.QUERY_LIST);return queryList(sql);}/*** 查询用户列表** @return*/public List<BaseUserEntity> getUserList(int start, int offset) {logger.infof("查询分页");String sql= properties.getProperty("queryPage", UserQuerySQL.QUERY_LIST_PAGE);return queryList(sql, start * offset, offset);}/*** 查询用户列表** @return*/public List<BaseUserEntity> getUserBySearch(String key) {logger.infof("用户名或邮箱查询 %s", key);String sql = properties.getProperty("queryByKeyword", UserQuerySQL.QUERY_BY_KEYWORD);return queryList(sql, key, key);}/*** 查询用户列表** @return*/public List<BaseUserEntity> getUserBySearch(String key, int start, int offset) {logger.infof("用户名或邮箱分页查询 %s, %d, %d", key, start, offset);String sql = properties.getProperty("queryByKeywordPage", UserQuerySQL.QUERY_BY_KEYWORD_PAGE);return queryList(sql, key, key, start * offset, offset);}/*** 保存用户** @param username* @return*/public BaseUserEntity saveUser(String username) {SnowflakeIdUtils idWorker = new SnowflakeIdUtils(1, 1);BaseUserEntity userEntity = new BaseUserEntity();userEntity.setUsername(username);userEntity.setId(String.valueOf(idWorker.nextId()));userEntity.setCreateTime(LocalDateTime.now());try (Connection c = DBConnectionPool.getInstance().getConnection(model.getId())) {DBConnectionPool.getInstance().q(model.getId()).update("insert into sys_user (user_id, username, create_time) values (?, ?, ?)",userEntity.getId(),username,userEntity.getCreateTime());return userEntity;} catch (Exception e) {e.printStackTrace();return null;}}/*** 删除用户** @param userId* @return*/public boolean deleteUser(String userId) {try (Connection c = DBConnectionPool.getInstance().getConnection(model.getId())) {int result = DBConnectionPool.getInstance().q(model.getId()).update("delete from sys_user where user_id = ?", userId);return result > 0;} catch (Exception e) {e.printStackTrace();return false;}}/*** 查询** @return*/protected BaseUserEntity queryOne(String sql, String... params) {try (Connection c = DBConnectionPool.getInstance().getConnection(model.getId())) {//从数据库查询BaseUserEntity baseUser = DBConnectionPool.getInstance().q(model.getId()).query(sql, new BeanHandler<>(BaseUserEntity.class), params);return baseUser;} catch (SQLException e) {e.printStackTrace();return null;}}/*** 查询列表** @param sql* @param params* @return*/protected List<BaseUserEntity> queryList(String sql, Object... params) {try (Connection c = DBConnectionPool.getInstance().getConnection(model.getId())) {//从数据库查询List<BaseUserEntity> baseUserList = DBConnectionPool.getInstance().q(model.getId()).query(sql, new BeanListHandler<>(BaseUserEntity.class), params);return baseUserList;} catch (SQLException e) {e.printStackTrace();return Collections.emptyList();}}/*** 用户对象转keycloak对象** @param baseUserList* @param session* @param realmModel* @param model* @return*/public static List<UserModel> baseUser2UserModel(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;}
}

我用了hikaricp连接池,并使用了commons-dbutils JDBC工具包连接,你可以替换成你自己的,连接池的封装我就不贴出来了。

2.5 插件入口,用户配置 BaseUserStorageProviderFactory

用户查询的核心逻辑我们都写完了,那么如何读取配置,如何将配置加载到插件中,这里就需要实现用户入口的工厂类了

我们创建一个 BaseUserStorageProviderFactory 并实现 UserStorageProviderFactory 接口,在接口中读取配置文件,包括我们上面说的,数据库查询语句的sql文件。

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.PROPERTIES_FILENAME).label("配置文件名").type(ProviderConfigProperty.STRING_TYPE).defaultValue("user-query.properties").helpText("查询配置文件名,文件和jar在同一目录").add().build();}/*** 初始化,从配置文件读取查询的sql** @param config*/@Overridepublic void init(Config.Scope config) {}@Overridepublic BaseUserStorageProvider create(KeycloakSession session, ComponentModel model) {try {logger.infof("创建用户提供器,开始加载数据库连接池... %s", model.getConfig().getFirst(DBConnectionPool.CONFIG_KEY_JDBC_URL));//初始化数据库连接DBConnectionPool.getInstance().initDB(model);//读取用户的sql配置文件,我们将配置文件和插件ID进行了关联,因为一个插件可以创建多个实例,每个实例配置都不一样,我们将ID和sql配置文件放在了map中进行管理Properties properties = DBConnectionPool.getInstance().getProperties(model.getId());BaseUserStorageProvider provider = new BaseUserStorageProvider(model, session, properties);return provider;} catch (Exception e) {throw new RuntimeException(e);}}/*** 提供器名称,在控制台中选择的时候显示*/@Overridepublic String getId() {return "custom-user-provider";}@Overridepublic String getHelpText() {return "自定义用户提供器";}@Overridepublic List<ProviderConfigProperty> getConfigProperties() {return configMetadata;}/*** 验证数据库连接是否正常** @param session* @param realm* @param config* @throws ComponentValidationException*/@Overridepublic void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {//初始化数据库连接MultivaluedHashMap<String, String> configMap = config.getConfig();DataSource dataSource = new HikariCP().init(configMap);try (Connection c = dataSource.getConnection()) {if (!c.isValid(20000)) {throw new ComponentValidationException("连接数据库超时");}} catch (Exception e) {System.out.println(e.getMessage());throw new ComponentValidationException("无法校验数据库连接", e);}}
}

这样我们就完成了主要代码的开发

2.6 添加插件的配置

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

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


在org.keycloak.storage.UserStorageProviderFactory文件中写入刚才创建的入口factory类的路径
比如我们的BaseUserStorageProviderFactory,带完整包名

com.test.keycloak.user.BaseUserStorageProviderFactory

2.7 打包部署

现在回到IDEA中,将我们刚才创建的项目打包成jar文件,然后找打keycloak的程序路径,将jar文件放到providers 文件夹下,如果有额外的依赖包,将依赖包一起复制到此处。如果你参考了我的实例,用户查询的sql语句放在了配置文件中,那么同时将自定的sql语句配置文件也放在此目录下,运行keycloak程序

3 配置插件

3.1 配置

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

点击此项后,系统会自动跳转到设置页面


填入你的配置,最后一项的配置文件名,就是我们的SQL语句配置文件,该文件需要和jar文件一同放在providers目录下,然后点击保存,设置正确的话,就会显示保存成功。

3.2 自定义属性(不需要可跳过)

现在回到用户列表页面,不出意外的话,列表页面应该就显示了你自己表中的用户了,点击任一个用户进入详情,在属性页面我们可以看到用户的自定义字段,就是我们在Adapter中设置的字段。

这些字段如果需要在客户端开发时获取到,还需要在客户端的mappers中进行设置,我们回到客户端菜单中,点击Mappers标签

图上是我已经添加好的电话号码字段,你可以自己添加一个,注意添加的时候,映射器类型需要选择为 User Attribute

本次内容我们不详细介绍mappers的使用,以后再单开一篇详细介绍

这样我们就完成了所有的开发和配置了,上面我没有把所有代码都贴出,你可以根据官方文档查询相关内容,或给我留言

有私信需要两个数据库连接工具类的,我把这两个工具类放在了gitee上,需要的可以自取
https://gitee.com/ssbp/share

(四)keycloak 自定义用户(SPI)开发相关推荐

  1. 微信开发学习总结(四)——自定义菜单(5)——个性化菜单接口

    一.个性化菜单接口说明 为了帮助公众号实现灵活的业务运营,微信公众平台新增了个性化菜单接口,开发者可以通过该接口,让公众号的不同用户群体看到不一样的自定义菜单.该接口开放给已认证订阅号和已认证服务号. ...

  2. keycloak SPI 开发讲解

    目标 1.在keycloak3.4.0版本,实现对于用户登录登出事件记录 2.将用户登录的最近一次时间记录至用户属性表中 需要掌握的知识点 1.keycloak SPI开发流程介绍,参考 keyclo ...

  3. keycloak User Storage SPI

     一 , 二为官网内容简介,  三为楼主实战案例, 通过插件项目打成jar包去部署到keycloak二进制文件目录下完成jar包部署相关功能 目录 一, 简介 二, 流程概述 1.提供者接口 2. 提 ...

  4. ASP.NET自定义控件组件开发 第四章 组合控件开发CompositeControl

    第四章 组合控件开发CompositeControl 大家好,今天我们来实现一个自定义的控件,之前我们已经知道了,要开发自定义的控件一般继承三个基 类:Control,WebControl,还有一个就 ...

  5. matlab有限元分析与应用_专栏 | UEL用户子程序开发步骤—有限元理论基础及Abaqus内部实现方式研究系列20...

    作者介绍 snowwave02 博士,高级工程师 snowwave02团队:设计仿真领域的软件开发团队,由软件.机械.物理等专业人员组成,10年以上CAD/CAE软件开发经验,精通Abaqus二次开发 ...

  6. keycloak 自定义登录页面

    keycloak 自定义登录页面详细步骤如下: 因为keycloak是jboss开发的,使用FTL后缀开发前端文件,可能根据以下方式实现 keycloak将前端页面分为四类:按类设置主题. •Acco ...

  7. 自定义算子高性能开发

    自定义算子高性能开发 在计图中,一共有三种方法来开发自定义的算子: 使用元算子进行组合. 使用Code算子开发自定义算子. 使用计图编译器编译自定义的模块和custom op. 其中,元算子开发是最为 ...

  8. asp.net core系列 47 Identity 自定义用户数据

    一.概述 接着上篇的WebAppIdentityDemo项目,将自定义用户数据添加到Identity DB,自定义扩展的用户数据类应继承IdentityUser类, 文件名为Areas / Ident ...

  9. javaweb学习总结(二十四):jsp传统标签开发

    一.标签技术的API 1.1.标签技术的API类继承关系 二.标签API简单介绍 2.1.JspTag接口 JspTag接口是所有自定义标签的父接口,它是JSP2.0中新定义的一个标记接口,没有任何属 ...

  10. Android Studio自定义模板 做开发竟然可以如此轻松 后篇

    ###1.概述 最近有很多人反馈,有些哥们不喜欢看文字性的东西,还有一些哥们根本就不知道我在搞啥子,那么以后我就采用博客加视频的方式,我们可以选择看视频讲解:http://pan.baidu.com/ ...

最新文章

  1. Cobalt Strike 的安装与简单使用
  2. eclipse修改默认工作空间
  3. containerd 与安全沙箱的 Kubernetes 初体验
  4. 牛客题霸 NC15 求二叉树的层序遍历
  5. mysql可以存储整数数值的是_MySQL的数值类型
  6. trident State应用指南
  7. 计组—双端口与多模块存储器
  8. go语言值得学习的开源项目推荐
  9. php在指定html元素中输出,如何从PHP中的数组输出html svg元素?
  10. python双划线_Python中单下划线(_)和双下划线(__)的特殊用法
  11. 干货~powershell与bash和docker在项目中怎么用
  12. MyEclipse安装配置maven插件
  13. android camera 竖直拍照 获取竖直方向照片 做缩放处理
  14. ctype函数_Ctype函数简介
  15. 服务器运维有夜班吗,运维倒班之所获
  16. 读书笔记-财务报表分析的目的
  17. P3426 [POI2005]SZA-Template(kmp、dp)
  18. STM32 USB HID设置(STM32CubeMX)
  19. html+css+动画过渡做遮罩层
  20. 让人懵逼的宏定义赋值

热门文章

  1. 使用 乐吾乐topology 遇到的问题解决方法汇总
  2. 机器翻译和自动译后编辑
  3. 嵌入式中的人工神经网络
  4. mybatis order by concat用法
  5. 计算机加域后数据库无法登录,[MDT] 解决因加域客户端 Windows 登录身份引发的无法打开登录所请求的数据库故障...
  6. 磁盘阵列恢复方法以及注意事项
  7. H3C_利用设置缺省静态路由优先级实现出口双线路的主备功能
  8. 如何在ionic官网打包自己的App
  9. 页面自动添加font标签
  10. 怎么用电脑录音,在电脑上录制音频的方法