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

升级Spring Boot 3后PATCH接口出现404问题求助

问题描述

将Kotlin API迁移到Spring Boot 3后,出现部分接口无法访问的情况:

  • GET请求localhost:8081/search/hello可正常访问,对应端点代码:
@PreAuthorize("hasRole('ROLE_MYROLE')")
@GetMapping("/search/{somephrase}")
suspend fun getSomething(@PathVariable("somephrase") phrase: String): someResponse {
  return myService.getPhrase(phrase)
}
  • PATCH请求localhost:8081/update/resourceId返回404,对应端点代码:
@PreAuthorize("hasRole('ROLE_MYROLE')")
@PatchMapping("/update/{resourceId}")
suspend fun updateSomething(
  @PathVariable("resourceId") resourceId: Long,
  @RequestBody updateJson: JsonNode
): UpdateResponse {
  return recommendationService.update(updateJson, resourceId)
}

收到的404响应:

{
    "timestamp": "2023-02-12 09:59:50",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/search"
}

控制台日志片段:

: [preHandle] PATCH /error - operationId: 00000000000000000000000000000000 |  

当前Spring Security配置:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.DefaultSecurityFilterChain

@Configuration
@EnableWebSecurity
class ResourceServerConfiguration {
  @Bean
  fun configure(http: HttpSecurity): DefaultSecurityFilterChain? {
    http.authorizeHttpRequests()
      .requestMatchers("/csrf").permitAll()
      .requestMatchers("/**").authenticated()
      .and()
      .csrf()
      .disable()
      .oauth2ResourceServer().jwt().jwtAuthenticationConverter { AuthAwareTokenConverter().convert(it) }
    return http.build()
  }
}

Token转换器代码(用于扩展令牌声明):

import org.springframework.core.convert.converter.Converter
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter

class AuthAwareTokenConverter : Converter<Jwt, AbstractAuthenticationToken> {
  private val jwtGrantedAuthoritiesConverter: Converter<Jwt, Collection<GrantedAuthority>> =
    JwtGrantedAuthoritiesConverter()


  override fun convert(jwt: Jwt): AbstractAuthenticationToken {
    val claims = jwt.claims
    val principal = findPrincipal(claims)
    val authorities = extractAuthorities(jwt)
    return AuthAwareAuthenticationToken(jwt, principal, authorities)
  }

  private fun findPrincipal(claims: Map<String, Any?>): String {
    return if (claims.containsKey(CLAIM_USERNAME)) {
      claims[CLAIM_USERNAME] as String
    } else if (claims.containsKey(CLAIM_USER_ID)) {
      claims[CLAIM_USER_ID] as String
    } else {
      claims[CLAIM_CLIENT_ID] as String
    }
  }

  private fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
    val authorities = mutableListOf<GrantedAuthority>().apply { addAll(jwtGrantedAuthoritiesConverter.convert(jwt)!!) }
    if (jwt.claims.containsKey(CLAIM_AUTHORITY)) {
      @Suppress("UNCHECKED_CAST")
      val claimAuthorities = (jwt.claims[CLAIM_AUTHORITY] as Collection<String>).toList()
      authorities.addAll(claimAuthorities.map(::SimpleGrantedAuthority))
    }
    return authorities.toSet()
  }

  companion object {
    const val CLAIM_USERNAME = "user_name"
    const val CLAIM_USER_ID = "user_id"
    const val CLAIM_CLIENT_ID = "client_id"
    const val CLAIM_AUTHORITY = "authorities"
  }
}

class AuthAwareAuthenticationToken(
  jwt: Jwt,
  private val aPrincipal: String,
  authorities: Collection<GrantedAuthority>
) : JwtAuthenticationToken(jwt, authorities) {
  override fun getPrincipal(): String {
    return aPrincipal
  }
}

已尝试简化PATCH端点(仅返回字符串),问题依旧,推测根源在Spring Security配置。

排查与解决方案

1. 定位请求路径异常

响应中path字段显示为/search,但实际请求的是/update/resourceId,说明请求可能被过滤器/拦截器修改了路径,或者存在错误的转发规则。需检查项目中自定义的过滤器、拦截器是否干扰了请求路径的传递。

2. 修复JWT转换器的实例化问题

当前配置中每次调用转换器都新建AuthAwareTokenConverter实例,可能导致Spring上下文传递异常。修改配置为注入单例Bean:

@Configuration
@EnableWebSecurity
class ResourceServerConfiguration {

  @Bean
  fun authAwareTokenConverter(): AuthAwareTokenConverter {
    return AuthAwareTokenConverter()
  }

  @Bean
  fun configure(http: HttpSecurity): DefaultSecurityFilterChain {
    http.authorizeHttpRequests()
      .requestMatchers("/csrf").permitAll()
      .anyRequest().authenticated()
      .and()
      .csrf().disable()
      .oauth2ResourceServer()
        .jwt()
        .jwtAuthenticationConverter(authAwareTokenConverter())
    return http.build()
  }
}

3. 验证协程与Spring Boot 3兼容性

由于端点使用suspend函数,需确保协程依赖配置正确:

  • 确认引入kotlinx-coroutines-spring依赖
  • 检查控制器类是否添加@RestController注解,确保协程端点被正确扫描

4. 启用调试日志追踪请求流转

添加日志配置,打印Security过滤器链的详细执行过程:

logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.web=DEBUG

通过日志查看请求在过滤器链中的每一步流转,确认是否在某个环节被拦截或路径被篡改。

5. 检查HTTP方法支持

Spring Boot 3对HTTP方法的默认处理无变更,但需确保:

  • 控制器类上的路径前缀(如@RequestMapping)未覆盖@PatchMapping的路径
  • 没有其他Security配置类(如遗留的WebSecurityConfigurerAdapter)与当前配置冲突

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 13:51:29