Quarkus同时使用OIDC与JWT认证时JsonWebToken注入为空问题
问题背景
我正在开发一个Quarkus 3.24应用,需要支持两种认证方式:
- 用户通过OIDC(Keycloak)采用标准授权码流完成认证;
- 客户端(代理或设备)通过登录时生成的自签发短期JWT完成认证。
参考Quarkus文档中《支持多种HTTP认证机制》的内容后,我实现了一个自定义HttpAuthenticationMechanism,根据自定义请求头HELIX-AGENT选择使用OIDC或JWT认证机制。但实际运行中出现了两个核心问题:
- 选择JWT认证后,OIDC仍会触发处理逻辑,产生警告;
- 直接注入
JsonWebToken得到NullJsonWebToken,但从SecurityIdentity.principal能获取到有效的JWT主体信息。
自定义认证机制实现
@Alternative @Priority(1) @ApplicationScoped class CustomAwareJWTAuthMechanism @Inject constructor( private val jwt: JWTAuthMechanism, private val oidc: OidcAuthenticationMechanism ) : HttpAuthenticationMechanism { companion object { private const val HELIX_AGENT_HEADER = "HELIX-AGENT" } override fun authenticate( context: RoutingContext, identityProviderManager: IdentityProviderManager ): Uni<SecurityIdentity> { return selectMechanism(context).authenticate(context, identityProviderManager) } override fun getChallenge(context: RoutingContext): Uni<ChallengeData> { return selectMechanism(context).getChallenge(context) } override fun getCredentialTypes(): MutableSet<Class<out AuthenticationRequest?>?> { return mutableSetOf<Class<out AuthenticationRequest?>>().apply { addAll(jwt.credentialTypes) addAll(oidc.credentialTypes) } } override fun getCredentialTransport(context: RoutingContext): Uni<HttpCredentialTransport> { return selectMechanism(context).getCredentialTransport(context) } private fun selectMechanism(context: RoutingContext): HttpAuthenticationMechanism { return if (context.request().getHeader(HELIX_AGENT_HEADER) != null) jwt else oidc } }
问题详情
JWT生成逻辑
客户端登录时,服务端生成自签发JWT:
@POST @Path("/logon") @PermitAll fun logon(@Valid dto: LogonWorkerRequestDto): WorkerLogonResponseDto { val worker = workerService.create(dto) val token = Jwt .subject(worker.id.toString()) .preferredUserName(worker.name) .groups("worker") .sign() ?: throw IllegalStateException("Failed to create token") return WorkerLogonResponseDto(token) }
请求异常表现
客户端后续请求API时携带该JWT,并添加HELIX-AGENT头标识身份类型,但出现两个异常:
- OIDC不必要触发警告:
尽管已选择JWT认证机制,OIDC仍会尝试解析JWT,输出警告:OidcJsonWebTokenProducer - Identity is not associated with an access token. Inject either 'SecurityIdentity' or 'UserInfo' if you need both code and bearer token flows. - JsonWebToken注入异常:
在资源类中注入JsonWebToken得到NullJsonWebToken,但SecurityIdentity.principal包含有效的DefaultJwtCallerPrincipal:
请求结果:@Path("/secure") class HelloResource @Inject constructor( private val identity: SecurityIdentity, private val jwt: JsonWebToken, ) { @GET @Authenticated fun getAuthenticatedUser(): String { return "Authenticated user: ${identity.principal.name}, JWT subject: ${jwt.name}" } }GET /secure Authenticated user: Bruno12311, JWT subject: null
核心疑问
- 为何自定义机制选择JWT认证后,OIDC仍会尝试处理请求?
- 在Quarkus中使用多种认证机制时,这是预期行为吗?
- 如何确保自签发JWT仅由SmallRye JWT机制处理,完全不触发OIDC?
- 为何
JsonWebToken注入得到NullJsonWebToken,但SecurityIdentity.principal却包含有效的DefaultJwtCallerPrincipal?
问题解答
1. OIDC仍触发处理的原因
Quarkus的OIDC扩展默认注册了OidcJsonWebTokenProducer这个CDI生产者,它会尝试在容器中生成JsonWebToken实例,不管当前使用的是哪种认证机制。即使你的自定义认证机制选择了JWT,这个OIDC生产者仍会触发,检查当前身份是否关联OIDC的访问令牌,找不到就输出警告。
2. 是否属于预期行为
这不属于多认证机制的预期行为,但属于OIDC扩展的默认行为。OIDC扩展的CDI生产者不会感知自定义认证机制的选择逻辑,只要OIDC扩展在项目依赖中,它就会自动工作。
3. 确保自签发JWT仅由SmallRye JWT处理的方案
有两种可行方案:
- 方案一:禁用OIDC的JWT生产者
在application.properties中添加配置,关闭OIDC的OidcJsonWebTokenProducer:quarkus.oidc.json-web-token-producer.enabled=false - 方案二:精细化路径与令牌判断
除了检查HELIX-AGENT头,还可以在selectMechanism中添加令牌类型判断(比如根据签名密钥区分自签发JWT和OIDC令牌);同时通过quarkus.oidc.paths配置OIDC仅处理用户登录相关路径,客户端请求的路径由JWT机制单独处理。
4. JsonWebToken注入异常的原因
JsonWebToken存在两个CDI生产者:SmallRye JWT的JwtProducer和OIDC的OidcJsonWebTokenProducer。当OIDC的生产者优先级更高时,它会先被触发,发现当前身份不是OIDC生成的,就返回NullJsonWebToken,并被注入到字段中。而SecurityIdentity是由你选择的JWT认证机制正确生成的,因此其principal是有效的DefaultJwtCallerPrincipal。解决这个问题的关键就是关闭OIDC的JWT生产者(方案一),让SmallRye JWT的生产者正确生成实例。
内容的提问来源于stack exchange,提问作者Artur K.

