Quarkus应用首次用户登录时创建用户配置的最佳方案
正确实现方式
1. 先明确职责边界
SecurityIdentityAugmentor的核心作用是增强身份信息(比如补充角色、扩展属性),它会在每次请求时被触发,完全不适合用来处理用户数据库初始化这类需要一次性执行的副作用操作——这也是你现在遇到重复创建节点问题的根本原因。
2. 用认证事件监听实现一次性用户初始化
Quarkus提供了安全事件机制,你可以监听用户认证成功后的SecurityIdentityCreatedEvent事件,在事件回调里执行用户初始化逻辑,确保只在用户首次登录(或每次认证时检查后)执行一次创建操作。
实现代码示例:
import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.SecurityIdentityCreatedEvent; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.ObservesAsync; import io.smallrye.jwt.auth.principal.JWTCallerPrincipal; @ApplicationScoped public class UserInitializationListener { // 异步监听SecurityIdentity创建事件,避免阻塞认证流程 public void onSecurityIdentityCreated(@ObservesAsync SecurityIdentityCreatedEvent event) { SecurityIdentity identity = event.getSecurityIdentity(); if (identity.isAnonymous()) { return; } JWTCallerPrincipal caller = (JWTCallerPrincipal) identity.getPrincipal(); String email = caller.getClaim("email"); // 双重检查锁保证线程安全,防止并发请求重复创建用户 if (!isUserAlreadyInDB(email)) { synchronized (UserInitializationListener.class) { if (!isUserAlreadyInDB(email)) { String name = caller.getClaim("given_name"); String familyName = caller.getClaim("family_name"); addUserToDB(email, name, familyName); } } } } // 原有的用户存在性检查方法 private boolean isUserAlreadyInDB(String email) { // 实现Neo4j查询逻辑,返回用户是否存在的结果 return false; } // 原有的用户插入方法 private void addUserToDB(String email, String name, String familyName) { // 实现Neo4j插入用户节点的逻辑 } }
3. 还原身份增强器的本职工作
把RolesAugmentor里的用户初始化代码删掉,只保留角色增强的核心逻辑:
@ApplicationScoped public class RolesAugmentor implements SecurityIdentityAugmentor { @Override public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) { return Uni.createFrom().item(build(identity)); } private Supplier<SecurityIdentity> build(SecurityIdentity identity) { if (identity.isAnonymous()) { return () -> identity; } else { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); JWTCallerPrincipal caller = (JWTCallerPrincipal) identity.getPrincipal(); String email = caller.getClaim("email"); // 只保留角色增强逻辑 builder.addRoles(extractUserRoles(email)); return builder::build; } } private Set<String> extractUserRoles(String email) { // 实现从数据库或其他来源提取角色的逻辑 return Set.of("user"); } }
4. 额外优化建议
- 如果你的Neo4j操作是阻塞式的,可在事件监听方法中用
Uni.createFrom().runBlocking()包裹,避免阻塞事件线程:public Uni<Void> onSecurityIdentityCreated(@ObservesAsync SecurityIdentityCreatedEvent event) { SecurityIdentity identity = event.getSecurityIdentity(); if (identity.isAnonymous()) { return Uni.createFrom().voidItem(); } JWTCallerPrincipal caller = (JWTCallerPrincipal) identity.getPrincipal(); String email = caller.getClaim("email"); return Uni.createFrom().runBlocking(() -> { if (!isUserAlreadyInDB(email)) { synchronized (UserInitializationListener.class) { if (!isUserAlreadyInDB(email)) { String name = caller.getClaim("given_name"); String familyName = caller.getClaim("family_name"); addUserToDB(email, name, familyName); } } } }); } - 推荐用Neo4j的
MERGE语句替代手动先查后插,让数据库层面保证原子性,省去代码里的同步锁:
这样哪怕有并发请求,数据库也只会创建一次用户节点。MERGE (u:User {email: $email}) ON CREATE SET u.name = $name, u.familyName = $familyName
内容的提问来源于stack exchange,提问作者Riduidel
相关产品推荐
相关产品推荐

