错误分析

从日志可以看出:

Securing OPTIONS /api/stores/address/provinces
Set SecurityContextHolder to anonymous SecurityContext
Pre-authenticated entry point called. Rejecting access

问题:Spring Security 将 OPTIONS 请求视为需要认证的请求,但预检请求是匿名的,所以被拒绝了。

解决方案

1. 在 Security 配置中允许 OPTIONS 请求

创建或修改 Spring Security 配置类:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                // 允许所有 OPTIONS 请求
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 允许其他公开端点
                .requestMatchers("/api/auth/**").permitAll()
                // 其他请求需要认证
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }


}

2. 如果使用的是旧版本的 Spring Security

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors().and()
            .csrf().disable()
            .authorizeRequests()
                // 允许所有 OPTIONS 请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

  
}

3. 确保 JWT Filter 跳过 OPTIONS 请求

在你的 JwtAuthenticationFilter 中:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    // 跳过 OPTIONS 预检请求
    if ("OPTIONS".equals(request.getMethod())) {
        return true;
    }
    
    String requestPath = request.getServletPath();
    boolean result = Arrays.stream(WhiteList.whiteList).anyMatch(path -> {
        if (path.endsWith("/**")) {
            String basePath = path.substring(0, path.length() - 3);
            return requestPath.startsWith(basePath);
        }
        return path.equals(requestPath);
    });
    return result;
}

4. 确保 CORS 配置

@RestController
@CrossOrigin(
    origins = "http://localhost:5173", 
    allowCredentials = "true",
    allowedHeaders = {"Authorization", "Content-Type"},
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS}
)
@RequestMapping("/api/stores")
public class StoreController {
    // ... 控制器方法
}

或者 在WebMvcConfigurer 全局配置

@Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins(allowedOrigins.split(","))  // 使用配置文件中的值
                .allowedMethods(allowedMethods.split(","))
                .allowedHeaders(allowedHeaders)
                .allowCredentials(allowCredentials)
                .maxAge(maxAge);
    }

关键配置要点

  1. .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - 这是最重要的配置
  2. CORS 配置必须在 Security 配置中启用 - 使用 .cors()
  3. JWT Filter 必须跳过 OPTIONS 请求
  4. 确保 allowedMethods 包含 OPTIONS ,特别要注意这个坑爹的OPTIONS方法

验证方法

配置完成后,重启应用,你应该看到:

  1. OPTIONS 请求返回 200 状态码
  2. 后续的实际请求能正常携带 Authorization 头
  3. 不再出现 403 Forbidden 错误

这样配置后,CORS 预检请求就能正常通过,实际的业务请求也能正常进行 JWT 认证。