升级Spring Boot 3/JDK 17后遇AnnotationConfigurationException求助
近期完成技术栈升级: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
相关产品推荐
相关产品推荐

