在我们的应用程序中,我们使用 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
。
在以下场景中,注释效果很好:
@Path("auditions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
class AuditionResource(
@CurrentUser private val currentUser: User?
) {
// Endpoints
}
如您所见,当我们在每个请求中注入当前用户时,
@CurrentUser
注释将按预期工作。
但是,这可能会造成性能损失,因为并非每个端点总是需要从数据库加载当前用户。因此,我们希望扩展
@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?
我有一个类似的用例,我已经实现了如下。
首先确保您已经定义了一个
@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));
}
}
@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
,那么数据库将不会命中。
希望这对您有帮助。