基于每个端点注入当前用户(从数据库加载)

问题描述 投票:0回答:1

在我们的应用程序中,我们使用 Quarkus 和 SmallRye JWT 扩展(请参阅此处)来实现基于角色的 AuthN 和 AuthZ。

我们定义了一个注释

CurrentUser
:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CurrentUser

该注解在

UserProvider
中有具体实现:

import ---.User
import ---.UserRepository
import jakarta.enterprise.context.RequestScoped
import jakarta.enterprise.inject.Produces
import org.eclipse.microprofile.jwt.Claim

@RequestScoped
class UserProvider(
    private val userRepository: UserRepository,
    @Claim("upn") private val currentUserEmail: String?
) {

    @Produces
    @CurrentUser
    fun currentUser(): User? {
        return if (currentUserEmail != null) {
            userRepository.findByEmail(currentUserEmail)
        } else {
            null
        }
    }
}

您可以很容易地看到,

UserProvider
根据在潜在 JWT 中发现的
User
声明从数据库加载
upn

用例 1:这是有效的

在以下场景中,注释效果很好:

@Path("auditions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
class AuditionResource(
    @CurrentUser private val currentUser: User?
) {
// Endpoints
}

如您所见,当我们在每个请求中注入当前用户时,

@CurrentUser
注释将按预期工作。

用例 2:不工作

但是,这可能会造成性能损失,因为并非每个端点总是需要从数据库加载当前用户。因此,我们希望扩展

@CurrentUser
的使用来支持更细粒度的基础,例如如下:

@Path("auditions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
class AuditionResource {
    @PATCH
    fun foo(
        @CurrentUser currentUser: User?
    ) {
        println("HI")
    }
}

不幸的是,在这种情况下,

currentUser()
UserProvider
方法不会被调用。

问题

我们如何才能同时支持用例 1 和用例 2?

kotlin jwt quarkus smallrye
1个回答
0
投票

我有一个类似的用例,我已经实现了如下。

  • 首先确保您已经定义了一个

    @UserDefinition
    实体,另请参阅此链接

  • 然后在 Quarkus 的

    JpaIdentityStore
    中构建一个“钩子”,这将从 Quarkus 的 HttpAuthenticationMechanism 中调用,有关更多信息,请检查此链接。在这里,您将基本上告诉 Quarkus 如何检索用户:

@Singleton
@Priority(1)
public class UserIdentityProvider extends JpaIdentityProvider {

    @Override
    public SecurityIdentity authenticate(EntityManager em, UsernamePasswordAuthenticationRequest request) {
        try {
            var user = em.createQuery("from UserEntity u where u.username = ?1", UserEntity.class)
                    .setParameter(1, request.getUsername())
                    .getSingleResult();
            
            if (!UserEntity.passwordMatches(request.getPassword().getPassword(), user.password, user.salt)) {
                throw new AuthenticationFailedException("Username and/or password incorrect: %s".formatted(request.getUsername()));
            }

            logUser(em, user.username);
            return QuarkusSecurityIdentity.builder()
                    .setPrincipal(new QuarkusPrincipal(user.username))
                    .addRoles(user.roles)
                    .addAttribute("email", user.email)
                    .build();
        } catch (Exception e) {
            Log.errorf("Unknown user login attempt: '%s'", request.getUsername());
            throw new AuthenticationFailedException("Unknown User");
        }
    }

    private static void logUser(EntityManager em, String username) {
        em.getTransaction().begin();
        UserEntity.update("lastLoggedIn = ?1 where username = ?2", OffsetDateTime.now(), username);
        em.getTransaction().commit();
        Log.infof("User %s******* logged in", username.substring(0, 3));
    }
}
  • 我还假设您有类似的 JWT 身份验证:
@Path("/auth")
public class AuthenticationController {

    @Inject
    SecurityIdentity identity;

 
    @POST
    @Path("/token")
    @Authenticated
    public Response accessToken() {
        var username = this.identity.getPrincipal().getName();
        var email = this.identity.getAttribute("email").toString();
        var roles = this.identity.getRoles();

        return Response
                .ok(createUserInfo(username, email, roles))
                .cookie(cookie("access_token", generateAccessToken()))
                .build();
    }

    // will use exp.time, issuer & audience from application.properties automatically (if set)
    private String generateAccessToken() {
        return Jwt.upn(this.identity.getPrincipal().getName())
                .subject(this.identity.getPrincipal().getName())
                .groups(this.identity.getRoles())
                .sign();
    }

    private NewCookie cookie(String name, String value) {
        return new NewCookie.Builder(name)
                .secure(true)
                .sameSite(STRICT)
                .httpOnly(true)
                .path("/")
                .value(value)
                .build();
    }
   
    private Map<String, Object> createUserInfo(String username, String email, Set<String> roles) {
        return Map.of(
                "username", username,
                "roles", roles,
                "email", email);
    }
}

这就是 http basic auth 时的流程。请求进来:

http 基本身份验证请求到 POST:/auth/token -> HttpAuthenticationMechanism 拦截 -> 转发到 UserIdentityProvider -> 从数据库检索用户 -> 创建 SecurityIdentity -> 从安全链转发到 AuhtenticationController。

如果您的端点包含

@PermitAll
,那么数据库将不会命中。

希望这对您有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.