API 迁移到 Spring Boot 3 后 Spring Security 无法工作

问题描述 投票:0回答:1

软件版本

  • Spring Boot 和依赖项:3.2.2
  • Spring 核心:6.1.3
  • 春季安全6.2.1
  • 码头服务器:11.0.20
  • 语言:Java 17、Kotlin(Jetbrains Kotlin 和 Kotlin 测试库版本)1.8.10

下面是基于 Jetty 的 Http 服务器配置、API 配置和 API 安全配置的修改代码片段,现在使用 SecurityFilterChain 而不是 WebSecurityConfigurerAdapter。

Http 服务器配置

@Configuration
@EnableConfigurationProperties(ApiServiceProperties::class)
@ComponentScan("com......service")
@Import(value = [ApiSecurityConfig::class, WebFluxConfig::class])
class HttpServerConfig(var apiServiceProperties: ApiServiceProperties) {

    /**
     * Jetty Server Bean.
     */
    @Bean
    @SuppressWarnings("LongMethod")
    fun jettyServer(
        context: ApplicationContext,
        springSecurityFilterChain: Filter,
        mdcSetterFilter: MdcSetterFilter,
        webContextFilter: WebContextFilter
    ): Server {
        LOG.info(
            "Starting Jetty server with " + "<some custom properties are being printed here>"
        )

        .. code removed ..

        ServletContextHandler(server, "").apply {
            val servlet = JettyHttpHandlerAdapter(WebHttpHandlerBuilder.applicationContext(context).build())
            addServlet(ServletHolder(servlet), "/")

            addFilter(FilterHolder(mdcSetterFilter), "/*", EnumSet.of(DispatcherType.REQUEST))
            addFilter(FilterHolder(webContextFilter), "/*", EnumSet.of(DispatcherType.REQUEST))

            // The ping endpoint should be unsecured, therefore ignored by the security filter
            addFilter(
                FilterHolder { request: ServletRequest, response: ServletResponse, chain: FilterChain ->
                    if (request is HttpServletRequest && request.requestURI != "/v1/ping") {
                        springSecurityFilterChain.doFilter(request, response, chain)
                    } else {
                        chain.doFilter(request, response)
                    }
                },
                "/v1/*",
                EnumSet.of(DispatcherType.REQUEST)
            )
        }.start()

        .. code removed ..

        server.start()

        LOG.info("Started Jetty server.")
        return server
    }

    .. code removed ..
}

API配置

@Configuration
@ComponentScan(basePackages = [
    "com......security",
    "com......service"
])
@EnableConfigurationProperties(ApiServiceProperties::class)
@Import(HttpServerConfig::class)
class ApiServiceConfig : AbstractSpringBasedApplicationConfig()

API安全配置

@Configuration
@EnableWebSecurity
@ComponentScan("com......security", "com......service")
@EnableMethodSecurity(prePostEnabled = false, jsr250Enabled = true)
class ApiSecurityConfig(
    private val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
    private val restAuthenticationProvider: RestAuthenticationProvider
) {
    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .cors { }
            .anonymous { it.disable() }
            .httpBasic { it.disable() }
            .formLogin { it.disable() }
            .logout { it.disable() }
            .csrf { it.disable() }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .exceptionHandling { it.authenticationEntryPoint(restAuthenticationEntryPoint) }
            .authenticationManager { authentication -> restAuthenticationProvider.authenticate(authentication) }
            .addFilterBefore(RestAuthenticationTokenFilter(), AnonymousAuthenticationFilter::class.java)
            .authorizeHttpRequests { it.requestMatchers("/**").permitAll().anyRequest().authenticated() }
        return http.build()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration(
            "/**",
            CorsConfiguration().applyPermitDefaultValues().apply {
                allowedMethods = listOf("POST", "GET", "PUT", "DELETE", "HEAD")
            }
        )
    }
}

自定义身份验证提供商

@Component
class RestAuthenticationProvider(
    private val securityServiceClient: SecurityServiceClient,
    private val cryptoService: CryptoService
) : AuthenticationProvider {

    /**
     * Given a [token] and [verifiedTokenModel], return a new User with granted authorities.
     */
    private fun createAuthenticatedUser(token: String, verifiedTokenModel: VerifiedTokenModel) = User
        .withUsername(verifiedTokenModel.verifiedPrincipalModel.id)
        .password(token)
        .authorities(verifiedTokenModel.verifiedPrincipalModel.scopes.map { scope -> 
            SimpleGrantedAuthority("ROLE_${scope.toUpperCase()}")
        })
        .build()

    /**
     * Given a [verifiedTokenModel], create a JSON Web Token to represent the authorizations of the verified principal.
     */
    private fun createJwt(verifiedTokenModel: VerifiedTokenModel) = cryptoService.createAuthToken(
        .. code removed ..
    )

    override fun authenticate(authentication: Authentication): Authentication? =
        (authentication as? RestAuthenticationToken)?.token?.let { token ->
            try {
                val verifiedTokenModel = securityServiceClient.verifyToken(token)
                val user = createAuthenticatedUser(token = token, verifiedTokenModel = verifiedTokenModel)
    
                RestAuthenticationToken(
                    .. code removed ..
                    jwt = createJwt(verifiedTokenModel = verifiedTokenModel)
                )
            } catch (e: ReplyException) {
                .. code removed ..
            }
        }


    .. code removed ..
}

下面是安全配置的新旧代码对比(Spring Boot 2.6.2 和 Spring Core 5.3.14)

Postman 请求总是收到 403 响应

日志

DEBUG c.a.e.d.api.v1.security.MdcSetterFilter : Setting MDC logging context.
DEBUG c.a.e.d.a.v1.security.WebContextFilter  : Setting WebContext on message
DEBUG o.s.security.web.FilterChainProxy       : Securing GET /v1/clients/*/brands  
INFO  c.a.e.d.api.v1.config.HttpServerConfig  : Token :: <bearer token value is printed here>
...
...
DEBUG o.s.w.s.adapter.HttpWebHandlerAdapter   : [49377233] HTTP GET "/v1/clients/*/brands"
...
DEBUG s.w.r.r.m.a.RequestMappingHandlerMapping: [49377233] Mapped to com......service.ClientsApiController#listBrands(String, ServerHttpRequest)
DEBUG AuthorizationManagerBeforeMethodInterceptor: Authorizing method invocation ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com......service.ClientsApiController.listBrands(..); target is of class [com......service.ClientsApiController]

DEBUG AuthorizationManagerBeforeMethodInterceptor: Failed to authorize ReflectiveMethodInvocation: public org.springframework.http.ResponseEntity com......service.ClientsApiController.listBrands(...); target is of class [com......service.ClientsApiController] with authorization manager org.springframework.security.config.annotation.method.configuration.DeferringObservationAuthorizationManager@2323fe6a and decision AuthorityAuthorizationDecision [granted=false, authorities=[ROLE_READ_BRANDS]]
DEBUG s.w.r.r.m.a.RequestMappingHandlerAdapter: [49377233] Using @ExceptionHandler com......service.DefaultExceptionHandler#onThrowable(Throwable, ServerWebExchange)  
DEBUG o.s.w.s.adapter.HttpWebHandlerAdapter   : [49377233] Completed 403 FORBIDDEN 
  • 已按照几个类似线程的建议尝试了安全链的多种组合
  • 在安全配置代码中添加了更多日志语句来捕获这些事件并帮助了解新流程的工作原理
  • RestAuthenticationProvider.authenticate() 中添加的 DEBUG 级别日志语句未显示在日志中,表明它没有被调用,并且流程在到达该点之前就中断了。

但是我怀疑配置的 AuthenticationProvider (之前尝试使用 http.authenticationProvider(..) ,但这也不起作用)和 AuthenticationManager 由于某种原因没有插入链中。需要社区的帮助来指导我正确设置。谢谢你。

spring-boot kotlin spring-security
1个回答
0
投票

通过对 SecurityFilterChain 进行以下更改,我最终能够使上述设置正常工作:

  1. 删除自定义 RestAuthenticationTokenFilter
  2. 删除了直接在 HttpSecurity 对象上设置自定义 AuthenticationProvider/AuthenitcationManager 的调用,它只是不起作用。
  3. 创建一个自定义 BearTokenAuthenticationConverter,其中包含与上述自定义过滤器中存在的相同逻辑,以通过从 servlet 请求中提取生成承载令牌来生成身份验证令牌。
  4. 向 ApiSecurityConfig 类添加了一个函数,用于创建嵌入了自定义 AuthenticationProvider 的 AuthenticationManager 对象
  5. 向 ApiSecurityConfig 类添加了一个函数,用于创建 AuthenticationManagerResolver 对象,并嵌入这个新的 AuthenticationManager
  6. 向 ApiSecurityConfig 类添加了一个函数,以使用其构造函数创建 AuthenticationFilter 的对象,该构造函数以上面创建的 AuthenticationManagerResolver 和 BearerTokenAuthenticationConverter 作为参数
  7. 通过传递此 AuthenticationFilter 对象来替换使用方法 addFilterBefore() 在 HttpSecurity 中设置自定义 RestAuthenticationTokenFilter 对象的调用

下面是 ApiSecurityConfig 类的修改后的代码片段

@Configuration
@EnableWebSecurity
@ComponentScan("com.ads.ep.domain.api.v1.security", "com.ads.ep.domain.api.v1.service")
@EnableMethodSecurity(prePostEnabled = false, securedEnabled = false, jsr250Enabled = true)
class ApiSecurityConfig(
    private val restAuthenticationEntryPoint: RestAuthenticationEntryPoint,
    private val restAuthenticationProvider: RestAuthenticationProvider
) {
    @Bean
    fun securityFilterChain(http: HttpSecurity, authManager: AuthenticationManager): SecurityFilterChain {
        http
            .cors { }
            .anonymous { it.disable() }
            .httpBasic { it.disable() }
            .formLogin { it.disable() }
            .logout { it.disable() }
            .csrf { it.disable() }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .exceptionHandling { it.authenticationEntryPoint(restAuthenticationEntryPoint) }
            .addFilterAfter(authenticationFilter(), AnonymousAuthenticationFilter::class.java)
            .authorizeHttpRequests { it.requestMatchers("/**").authenticated() }
        return http.build()
    }

    @Bean
    fun authenticationManager(): AuthenticationManager {
        return AuthenticationManager { authentication: Authentication -> restAuthenticationProvider.authenticate(authentication) }
    }

    @Bean
    fun resolver(): AuthenticationManagerResolver<HttpServletRequest> {
        return AuthenticationManagerResolver {
            authenticationManager()
        }
    }

    private fun authenticationFilter(): AuthenticationFilter {
        LOG.info("Building authentication filter")
        val filter: AuthenticationFilter = AuthenticationFilter(resolver(), BearTokenAuthenticationConverter())
        filter.successHandler = AuthenticationSuccessHandler {
                _: HttpServletRequest?, _: HttpServletResponse?, _: Authentication? -> }
        return filter
    }

    /**
     * Enable CORS for all paths.
     */
    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration(
            "/**",
            CorsConfiguration().applyPermitDefaultValues().apply {
                allowedMethods = listOf("POST", "GET", "PUT", "DELETE", "HEAD")
            }
        )
    }

    companion object {
        private val LOG: Logger = LoggerFactory.getLogger(ApiSecurityConfig::class.java)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.