You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级Spring Boot 3/JDK 17后遇AnnotationConfigurationException求助

Spring Boot 3升级后继承仓库方法的@PreAuthorize元注解重复识别问题

近期完成技术栈升级:Spring Boot从2.6.4升级到3,Kotlin从1.7.20升级到1.8.0,JDK从11升级到17。升级后触发AnnotationConfigurationException,异常由继承自基础仓库的方法上的@PreAuthorize元注解引发。

报错信息

org.springframework.core.annotation.AnnotationConfigurationException: Found more than one annotation of type interface org.springframework.security.access.prepost.PreAuthorize attributed to public final java.util.List jdk.proxy2.$Proxy390.findAll() Please remove the duplicate annotations and publish a bean to handle your authorization logic.
        at org.springframework.security.authorization.method.AuthorizationAnnotationUtils.findUniqueAnnotation(AuthorizationAnnotationUtils.java:64)
        at org.springframework.security.authorization.method.PreAuthorizeExpressionAttributeRegistry.findPreAuthorizeAnnotation(PreAuthorizeExpressionAttributeRegistry.java:71)
        at org.springframework.security.authorization.method.PreAuthorizeExpressionAttributeRegistry.resolveAttribute(PreAuthorizeExpressionAttributeRegistry.java:61)
        at org.springframework.security.authorization.method.AbstractExpressionAttributeRegistry.lambda$getAttribute$0(AbstractExpressionAttributeRegistry.java:57)
        at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
        at org.springframework.security.authorization.method.AbstractExpressionAttributeRegistry.getAttribute(AbstractExpressionAttributeRegistry.java:57)
        at org.springframework.security.authorization.method.AbstractExpressionAttributeRegistry.getAttribute(AbstractExpressionAttributeRegistry.java:46)
        at org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager.check(PreAuthorizeAuthorizationManager.java:63)
        at org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager.check(PreAuthorizeAuthorizationManager.java:40)
        at org.springframework.security.authorization.ObservationAuthorizationManager.check(ObservationAuthorizationManager.java:55)
        at org.springframework.security.config.annotation.method.configuration.DeferringObservationAuthorizationManager.check(DeferringObservationAuthorizationManager.java:47)
        at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.attemptAuthorization(AuthorizationManagerBeforeMethodInterceptor.java:252)
        at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.invoke(AuthorizationManagerBeforeMethodInterceptor.java:198)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
        at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)

调试现象

Spring Security会重复识别单个@PreAuthorize注解。将findAll方法从基础仓库迁移到实际实体仓库后,异常不再出现。

相关代码实现

基础仓库BaseRepository

定义通用CRUD方法并通过元注解配置权限:

@NoRepositoryBean
interface BaseRepository<T : BaseEntity> : CrudRepository<T, Long> {

    /* [...] */

    @RestResource
    @HasRoleAdminOrHasReadCollectionPermission
    override fun findAll(): List<T>

    /* [...] */

}

实体仓库CategoryRepository

继承基础仓库并配置REST资源路径:

@RepositoryRestResource(path = "categories", collectionResourceRel = "categories")
@Transactional(readOnly = true)
interface CategoryRepository : BaseRepository<Category> {

    /* [...] */

}

@HasRoleAdminOrHasReadCollectionPermission元注解

封装@PreAuthorize的自定义权限注解:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PreAuthorize("hasRole('$ROLE_ADMIN') or hasPermission($COLLECTION_TARGET, $DELETE_PERMISSION)")
annotation class HasRoleAdminOrHasDeleteCollectionPermission

补充:Application启动类

@SpringBootApplication(exclude = [ErrorMvcAutoConfiguration::class])
@ServletComponentScan
@EnableAsync
@EnableScheduling
class Application {
    companion object {
        val HOSTNAME = System.getenv("HOSTNAME")
        val TIMESTAMP = System.currentTimeMillis()

        /** 程序入口,通过Gradle `bootRun`任务启动 */
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(Application::class.java, *args)
        }
    }
}

补充:SecurityConfig安全配置类

@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity(prePostEnabled = true)
class SecurityConfig(
    private val environment: Environment,
    private val baseProperties: BaseProperties,
    private val accountService: AccountService,
    private val integrationService: IntegrationService,
    private val jwtService: JwtService,
    private val loginAttemptService: LoginAttemptService,
    private val handlerExceptionResolver: HandlerExceptionResolver
) {

    @Bean
    fun webSecurityCustomizer(): WebSecurityCustomizer =
        WebSecurityCustomizer { web: WebSecurity -> web.debug(true) }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = baseProperties.corsAllowedOrigins()
        configuration.allowedMethods = listOf(CorsConfiguration.ALL)
        configuration.allowedHeaders = listOf(CorsConfiguration.ALL)
        configuration.allowCredentials = true
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration(ANY_PATH, configuration)
        return source
    }

    @Bean
    fun userDetailsService(): UserDetailsService =
        AccountUserDetailsService(accountService, integrationService)

    @Bean
    fun authenticationManager(
        http: HttpSecurity,
        userDetailsService: UserDetailsService
    ): AuthenticationManager {
        val authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder::class.java)

        val loginAttemptDaoAuthenticationProvider = LoginAttemptDaoAuthenticationProvider(loginAttemptService, userDetailsService)
        val jwtPreAuthenticationProvider = PreAuthenticatedAuthenticationProvider()

        jwtPreAuthenticationProvider.setPreAuthenticatedUserDetailsService(UserDetailsByNameServiceWrapper(userDetailsService))

        return authenticationManagerBuilder
            .userDetailsService(userDetailsService).and()
            .authenticationProvider(loginAttemptDaoAuthenticationProvider)
            .authenticationProvider(jwtPreAuthenticationProvider)
            .build()
    }

    @Bean
    fun filterChain(
        http: HttpSecurity,
        authenticationManager: AuthenticationManager,
        userDetailsService: UserDetailsService
    ): SecurityFilterChain {
        return http
            .requireSSL()
            .disableLocalHSTS()
            .enableCors()
            .enableStatelessCsrf()
            .statelessSessionManagement()
            .jwtAuthentication(authenticationManager)
            .setRequestRestrictions()
            .build()
    }

    private fun HttpSecurity.requireSSL(): HttpSecurity =
        requiresChannel().anyRequest().requiresSecure().and()

    private fun HttpSecurity.disableLocalHSTS(): HttpSecurity =
        if (environment.acceptsProfiles(Profiles.of(DEVELOPMENT, TESTING)))
            headers().httpStrictTransportSecurity().disable().and()
        else this

    private fun HttpSecurity.enableCors(): HttpSecurity =
        cors().and()

    private fun HttpSecurity.enableStatelessCsrf(): HttpSecurity =
        csrf().disable().addFilterBefore(StatelessCsrfFilter(), CsrfFilter::class.java)

    private fun HttpSecurity.statelessSessionManagement(): HttpSecurity =
        sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

    private fun HttpSecurity.jwtAuthentication(
        authenticationManager: AuthenticationManager
    ): HttpSecurity =
        addFilterBefore(JwtPreAuthenticatedProcessingFilter(authenticationManager, jwtService), UsernamePasswordAuthenticationFilter::class.java)
            .addFilterBefore(JwtCookiePreAuthenticationFilter(jwtService), UsernamePasswordAuthenticationFilter::class.java)
            .formLogin()
            .loginProcessingUrl(LOGIN_URL)
            .successHandler(JwtCookieAuthenticationSuccessHandler(jwtService))
            .failureHandler(AuthenticationFailureResolver(handlerExceptionResolver))
            .and()
            .logout()
            .logoutUrl(LOGOUT_URL)
            .logoutSuccessHandler(Http200LogoutSuccessHandler())
            .addLogoutHandler(JwtCookieLogoutHandler(jwtService))
            .and()

    private fun HttpSecurity.setRequestRestrictions(): HttpSecurity =
        authorizeHttpRequests {
            it.requestMatchers(HttpMethod.GET, ROOT_URL).permitAll()
                .requestMatchers([...])
                .anyRequest().denyAll().and()
        }
}

已检查Kotlin生成的Java字节码,未发现重复注解,目前无法定位问题根源,寻求解决方案。


内容的提问来源于stack exchange,提问作者Lukas0610

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 21:50:33