Security Tips and Tricks
Quarkus Security Dependency
io.quarkus:quarkus-security
模块包含核心 Quarkus 安全类。
在大多数情况下,它不必直接添加到项目的构建文件中,因为所有安全扩展都已提供了它。但是,如果你需要编写自己的自定义安全代码(例如,注册一个 Custom Jakarta REST SecurityContext)或使用 BouncyCastle 库,那么请确保已包含该模块:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
implementation("io.quarkus:quarkus-security")
HttpAuthenticationMechanism Customization
可以通过注册 CDI 实现 Bean 来自定义 HttpAuthenticationMechanism
。在下面的示例中,自定义验证器委托给 quarkus-smallrye-jwt
提供的 JWTAuthMechanism
:
@Alternative
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism delegate;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
// do some custom action and delegate
return delegate.authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return delegate.getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return delegate.getCredentialTypes();
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport() {
return delegate.getCredentialTransport();
}
}
|
Dealing with more than one HttpAuthenticationMechanism
可以组合多个 HttpAuthenticationMechanism
,例如,必须使用 Quarkus 中提供的内置 Basic
或 JWT
机制来验证作为 HTTP Authorization
Basic
或 Bearer
架构值传递的服务客户端凭据,而必须使用 quarkus-oidc
中提供的 Authorization Code
机制来使用 Keycloak 或其他 OpenID Connect 提供程序对用户进行身份验证。
在这些情况下,机制逐次要求验证凭据,直至创建一个 SecurityIdentity
。机制按优先级降序排列。Basic
身份验证机制具有最优先级的 2000
,紧跟其后的是优先级为 1001
的 Authorization Code
机制,Quarkus 提供的所有其他机制的优先级为 1000
。
如果没有提供凭据,则创建机制特定的质询,例如,401
状态由 Basic
或 JWT
机制返回,重定向用户至 OpenID Connect 提供程序的 URL 由 quarkus-oidc
返回,依此类推。
因此,如果 Basic
和 Authorization Code
机制相结合,则如果没有提供凭据,则返回 401
,如果 JWT
和 Authorization Code
机制相结合,则返回重定向 URL。
在一些情况下,选择质询的这种默认逻辑恰好是给定应用程序所需的,但有时可能无法满足要求。在这种情况下(或在确实其他类似情况下,你需要更改请求机制处理当前身份验证或质询请求的顺序),你可以创建自定义机制并选择哪个机制应创建质询,例如:
@Alternative 1
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism jwt;
@Inject
OidcAuthenticationMechanism oidc;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return selectBetweenJwtAndOidc(context).authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
Set<Class<? extends AuthenticationRequest>> credentialTypes = new HashSet<>();
credentialTypes.addAll(jwt.getCredentialTypes());
credentialTypes.addAll(oidc.getCredentialTypes());
return credentialTypes;
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return selectBetweenJwtAndOidc(context).getCredentialTransport(context);
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) {
....
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidcChallenge(RoutingContext context) {
// for example, if no `Authorization` header is available and no `code` parameter is provided - use `jwt` to create a challenge
}
}
1 | 将机制声明为一个备用 bean 确保使用此机制,而不是 OidcAuthenticationMechanism 和 JWTAuthMechanism 。 |
Security Identity Customization
在内部,身份提供程序创建并更新 io.quarkus.security.identity.SecurityIdentity
类的实例,该实例保存用于对客户端(用户)及其其他安全属性进行身份验证的主体、角色和凭据。自定义 SecurityIdentity
的一个简单选项是注册自定的 SecurityIdentityAugmentor
。例如,下面的增强程序添加了一个附加角色:
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
// Do 'return context.runBlocking(build(identity));'
// if a blocking call is required to customize the identity
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
if(identity.isAnonymous()) {
return () -> identity;
} else {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
// add custom role source here
builder.addRole("dummy");
return builder::build;
}
}
}
这里有另一个示例,展示如何使用当前 mutual TLS (mTLS) authentication 请求中可用的客户端证书来添加更多角色:
import java.security.cert.X509Certificate;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
import java.util.Set;
@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) {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
CertificateCredential certificate = identity.getCredential(CertificateCredential.class);
if (certificate != null) {
builder.addRoles(extractRoles(certificate.getCertificate()));
}
return builder::build;
}
private Set<String> extractRoles(X509Certificate certificate) {
String name = certificate.getSubjectX500Principal().getName();
switch (name) {
case "CN=client":
return Collections.singleton("user");
case "CN=guest-client":
return Collections.singleton("guest");
default:
return Collections.emptySet();
}
}
}
如果注册了多个自定义 |
默认情况下,在增强安全身份时不会激活请求上下文,这意味着如果你想使用例如强制使用请求上下文的 Hibernate,你将拥有一个 jakarta.enterprise.context.ContextNotActiveException
。
解决办法是激活请求上下文,以下示例展示了如何使用 Panache UserRoleEntity
获取 Hibernate 中的角色。
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Inject
Instance<SecurityIdentitySupplier> identitySupplierInstance;
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
if(identity.isAnonymous()) {
return Uni.createFrom().item(identity);
}
// Hibernate ORM is blocking
SecurityIdentitySupplier identitySupplier = identitySupplierInstance.get();
identitySupplier.setIdentity(identity);
return context.runBlocking(identitySupplier);
}
}
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.control.ActivateRequestContext;
import java.util.function.Supplier;
@Dependent
class SecurityIdentitySupplier implements Supplier<SecurityIdentity> {
private SecurityIdentity identity;
@Override
@ActivateRequestContext
public SecurityIdentity get() {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
String user = identity.getPrincipal().getName();
UserRoleEntity.<userRoleEntity>streamAll()
.filter(role -> user.equals(role.user))
.forEach(role -> builder.addRole(role.role));
return builder.build();
}
public void setIdentity(SecurityIdentity identity) {
this.identity = identity;
}
}
上面示例中显示的 CDI 请求上下文激活无法帮助你访问启用主动身份验证时的 RoutingContext
。以下示例说明如何从 SecurityIdentityAugmentor
访问 RoutingContext
:
package org.acme.security;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context,
Map<String, Object> attributes) {
RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
if (routingContext != null) {
// Augment SecurityIdentity using RoutingContext
} else {
return augment(identity, context); 1
}
}
...
}
1 | 在 HTTP 请求完成后增强 SecurityIdentity 时 RoutingContext 不可使用。 |
如果你实施了一个自定的 |
Custom Jakarta REST SecurityContext
如果你使用 Jakarta REST ContainerRequestFilter
设置一个自定义 Jakarta REST SecurityContext
,则确保 ContainerRequestFilter
在 Jakarta REST 预匹配阶段运行,方法是为它添加一个 @PreMatching
注释,以便此自定义安全上下文与 Quarkus SecurityIdentity
链接,例如:
import java.security.Principal;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
@Provider
@PreMatching
public class SecurityOverrideFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String user = requestContext.getHeaders().getFirst("User");
String role = requestContext.getHeaders().getFirst("Role");
if (user != null && role != null) {
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return new Principal() {
@Override
public String getName() {
return user;
}
};
}
@Override
public boolean isUserInRole(String r) {
return role.equals(r);
}
@Override
public boolean isSecure() {
return false;
}
@Override
public String getAuthenticationScheme() {
return "basic";
}
});
}
}
}
Disabling Authorization
如果你有充分的理由禁用授权,则可以注册自定的 AuthorizationController
:
@Alternative
@Priority(Interceptor.Priority.LIBRARY_AFTER)
@ApplicationScoped
public class DisabledAuthController extends AuthorizationController {
@ConfigProperty(name = "disable.authorization", defaultValue = "false")
boolean disableAuthorization;
@Override
public boolean isAuthorizationEnabled() {
return !disableAuthorization;
}
}
对于手动测试,Quarkus 提供了一个方便的配置属性,用于在开发模式下禁用授权。此属性与上面显示的自定义 AuthorizationController
具有完全相同的效果,但仅在开发模式下可用:
quarkus.security.auth.enabled-in-dev-mode=false
另请参阅 TestingSecurity Annotation 部分,了解如何使用 TestSecurity
注释禁用安全检查。
Registering Security Providers
Default providers
在原生模式下运行时,GraalVM 原生可执行文件生成的默认行为是仅包含主要“SUN”提供程序,除非你已启用 SSL,在这种情况下会注册所有安全提供程序。如果你不使用 SSL,则可以使用 quarkus.security.security-providers
属性按名称有选择地注册安全提供程序。以下示例说明了注册“SunRsaSign”和“SunJCE”安全提供程序的配置:
quarkus.security.security-providers=SunRsaSign,SunJCE
BouncyCastle
如果你需要注册一个 org.bouncycastle.jce.provider.BouncyCastleProvider
JCE 提供程序,请设置一个 BC
提供程序名称:
quarkus.security.security-providers=BC
并添加 BouncyCastle 提供程序依赖项:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bcprov-jdk18on")
BouncyCastle JSSE
如果您需要注册 org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSE 提供程序并使用该提供程序代替默认的 SunJSSE 提供程序,请设置 BCJSSE
提供程序名称:
quarkus.security.security-providers=BCJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
并添加 BouncyCastle TLS 依赖项:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-jdk18on")
BouncyCastle FIPS
如果您需要注册 org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
JCE 提供程序,请设置 BCFIPS
提供程序名称:
quarkus.security.security-providers=BCFIPS
并添加 BouncyCastle FIPS 提供程序依赖项:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bc-fips")
|
BouncyCastle JSSE FIPS
如果您需要注册 org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSE 提供程序并使用该提供程序与 org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
结合使用,代替默认的 SunJSSE 提供程序,请设置 BCFIPSJSSE
提供程序名称:
quarkus.security.security-providers=BCFIPSJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.key-store-file-type=BCFKS
quarkus.http.ssl.certificate.key-store-provider=BCFIPS
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
quarkus.http.ssl.certificate.trust-store-file-type=BCFKS
quarkus.http.ssl.certificate.trust-store-provider=BCFIPS
以及针对使用 BouncyCastle FIPS 提供程序进行了优化的 BouncyCastle TLS 依赖项:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-fips</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-fips")
implementation("org.bouncycastle:bc-fips")
请注意,密钥库和信任库类型及提供程序已设置为 BCFKS
和 BCFIPS
。可以像这样使用此类型和提供程序生成密钥库:
keytool -genkey -alias server -keyalg RSA -keystore server-keystore.jks -keysize 2048 -keypass password -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider -providerpath $PATH_TO_BC_FIPS_JAR -storetype BCFKS
|
SunPKCS11
SunPKCS11
提供程序提供了一个桥梁连接到特定的 PKCS#11
实现,例如加密智能卡和其他硬件安全模块、FIPS 模式下的网络安全服务等。
通常,为了使用 SunPKCS11
,需要安装 PKCS#11
实现,生成一个配置,该配置通常引用一个共享库、令牌插槽等,并编写以下 Java 代码:
import java.security.Provider;
import java.security.Security;
String configuration = "pkcs11.cfg"
Provider sunPkcs11 = Security.getProvider("SunPKCS11");
Provider pkcsImplementation = sunPkcs11.configure(configuration);
// or prepare configuration in the code or read it from the file such as "pkcs11.cfg" and do
// sunPkcs11.configure("--" + configuration);
Security.addProvider(pkcsImplementation);
在 Quarkus 中,您只需在配置级别就能实现相同的功能,而无需修改代码,例如:
quarkus.security.security-providers=SunPKCS11
quarkus.security.security-provider-config.SunPKCS11=pkcs11.cfg
请注意,虽然在本地镜像中支持访问 |
Reactive Security
如果您打算在响应式环境中使用安全性,则可能需要 SmallRye Context Propagation:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-context-propagation")
这将允许您在整个响应式回调中传播身份。您还需要确保您正在使用能够传播身份的执行器(例如,没有 CompletableFuture.supplyAsync
),以确保 Quarkus 可以传播它。有关更多信息,请参阅 Context Propagation Guide。
Observe security events
Quarkus bean 可以使用 CDI observers 来使用身份验证和授权安全事件。观察者可以是同步的或异步的。
-
io.quarkus.security.spi.runtime.AuthenticationFailureEvent
-
io.quarkus.security.spi.runtime.AuthenticationSuccessEvent
-
io.quarkus.security.spi.runtime.AuthorizationFailureEvent
-
io.quarkus.security.spi.runtime.AuthorizationSuccessEvent
-
io.quarkus.oidc.SecurityEvent
-
io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent
有关 Quarkus OpenID Connect 扩展的特定安全事件的更多信息,请参阅 OIDC 代码流机制的 Listening to important authentication events 部分,用于保护 Web 应用程序指南。 |
package org.acme.security;
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import org.jboss.logging.Logger;
public class SecurityEventObserver {
private static final Logger LOG = Logger.getLogger(SecurityEventObserver.class.getName());
void observeAuthenticationSuccess(@ObservesAsync AuthenticationSuccessEvent event) { 1
LOG.debugf("User '%s' has authenticated successfully", event.getSecurityIdentity().getPrincipal().getName());
}
void observeAuthenticationFailure(@ObservesAsync AuthenticationFailureEvent event) {
RoutingContext routingContext = (RoutingContext) event.getEventProperties().get(RoutingContext.class.getName());
LOG.debugf("Authentication failed, request path: '%s'", routingContext.request().path());
}
void observeAuthorizationSuccess(@ObservesAsync AuthorizationSuccessEvent event) {
String principalName = getPrincipalName(event);
if (principalName != null) {
LOG.debugf("User '%s' has been authorized successfully", principalName);
}
}
void observeAuthorizationFailure(@Observes AuthorizationFailureEvent event) {
LOG.debugf(event.getAuthorizationFailure(), "User '%s' authorization failed", event.getSecurityIdentity().getPrincipal().getName());
}
private static String getPrincipalName(SecurityEvent event) { 2
if (event.getSecurityIdentity() != null) {
return event.getSecurityIdentity().getPrincipal().getName();
}
return null;
}
}
1 | 此观察器异步使用所有 AuthenticationSuccessEvent 事件,这意味着 HTTP 请求处理将继续进行,而不管事件处理如何。根据应用程序的不同,这可能是很多 AuthenticationSuccessEvent 事件。因此,异步处理对性能可能有积极影响。 |
2 | 由于它们都实现了 io.quarkus.security.spi.runtime.SecurityEvent 接口,因此所有支持的安全事件类型都可使用通用代码。 |