Keycloak自定义Authenticator调用addUser出现唯一索引冲突报错
问题根因
- 核心是竞态条件导致的唯一索引冲突:你的自定义认证器的
authenticate方法被两个不同的请求线程(task-6、task-8)几乎同时触发,两个线程在用户不存在的时间点先后执行getUserByUsername查询,都拿到user == null的结果,同时进入用户新增分支。 - task-6的
addUser操作先执行完成,成功写入数据库,后续顺利执行到isEnabled()的日志输出;task-8执行addUser时,数据库中已经存在相同REALM_ID+USERNAME的记录,触发唯一索引约束直接抛出异常,因此不会走到后续的日志步骤。 - 首次登录报错是因为抛出异常的线程将错误返回给前端,但此时用户已经被另一个线程创建成功,因此第二次登录时所有线程查询都能命中已存在的用户,不会进入新增逻辑,登录自然成功。
解决方案
你可以任选以下一种方案解决:
- 方案1:新增逻辑加分布式锁
以realmId + 用户名作为锁的key,在执行用户查询前就加锁,保证同一时间同一个域下的同一个用户名只有一个线程能执行新增操作,从根源避免竞态条件。 - 方案2:异常捕获兜底重试
把addUser的逻辑包裹在try-catch块中,捕获唯一键冲突的PersistenceException或者SQLException,异常触发后重新执行一次getUserByUsername查询,只要能查到已存在的用户,就直接使用该用户继续后续流程即可。
示例修改代码:public void authenticate(AuthenticationFlowContext context) { KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); String username = "hello"; UserProvider users = session.userLocalStorage(); UserModel user = users.getUserByUsername(realm,username); if (user == null) { logger.infof("addingUser"); try { user = session.users().addUser(realm,username); logger.infof("isEnabled()"); user.setEnabled(true); user.setEmail("mail@mail.com"); user.setEmailVerified(true); } catch (PersistenceException e) { // 捕获唯一键冲突异常,重新查询用户 user = users.getUserByUsername(realm,username); if (user == null) { // 极端情况才会走到这,直接抛出异常即可 throw e; } } } context.setUser(user); context.success(); } - 方案3:排查重复触发原因
先检查你的Keycloak认证流配置,确认自定义认证器没有被重复添加到流程中;同时排查前端登录逻辑,避免同一个登录操作重复发起两次请求,从调用侧减少重复触发的可能。
内容的提问来源于stack exchange,提问作者Sultan Zhumatayev
相关产品推荐
相关产品推荐

