我最近将我的Spring Boot应用程序升级到了Spring Boot 3(从2.6.4)、Kotlin 1.8.0(从1.7.20)和JDK 17(从11),现在我面临着由继承的存储库方法上使用的@PreAuthorize
元注解引起的AnnotationConfigurationExceptions。
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不知何故多次反映了单个注解,只要我将方法从基本存储库移动到实际存储库,异常就不再出现。
我有一个基本存储库,它实现了findAll、findById等一些常用方法,并使用REST-API将它们标记为可访问,使用@HasRoleAdminOrHasReadCollectionPermission
元注解限制访问(稍后介绍):
@NoRepositoryBean
interface BaseRepository<T : BaseEntity> : CrudRepository<T, Long> {
/* [...] */
@RestResource
@HasRoleAdminOrHasReadCollectionPermission
override fun findAll(): List<T>
/* [...] */
}
第二,基本存储库由实际的实体存储库实现:
@RepositoryRestResource(path = "categories", collectionResourceRel = "categories")
@Transactional(readOnly = true)
interface CategoryRepository : BaseRepository<Category> {
/* [...] */
}
第三,这里是@HasRoleAdminOrHasReadCollectionPermission
元注解:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PreAuthorize("hasRole('$ROLE_ADMIN') or hasPermission($COLLECTION_TARGET, $DELETE_PERMISSION)")
annotation class HasRoleAdminOrHasDeleteCollectionPermission
我完全没有主意了,看了Kotlin生成的Java文件,没有重复的注解。希望这里有人能帮我。
编辑:根据请求,下面是Application类和包含security-config的类:
@SpringBootApplication(exclude = [ErrorMvcAutoConfiguration::class])
@ServletComponentScan
@EnableAsync
@EnableScheduling
class Application {
companion object {
val HOSTNAME = System.getenv("HOSTNAME")
val TIMESTAMP = System.currentTimeMillis()
/** Entry point, run with gradle `bootRun` task. */
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
}
}
@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()
}
}
1条答案
按热度按时间62lalag41#
经过更多的调试和阅读migration-guide @varad后,我决定实现定制的授权管理器,它不像Spring Security那样彻底地扫描注解。
我在GitHub上发布了自定义方法-安全和授权管理器的实现:https://gist.github.com/Lukas0610/5ac78a358276fa15b1ce7cdc0483988d
谢谢你们的帮助。