发了 /user/getBrowseDetail和/user/getResponDetail,但为什么进入handle里面有三次?且第一次的handle类型是AbstractHandleMapping$PreFlightHttpRequestHandler@xxx,这一次进来的时候flag为false,StpUtils.checkLogin抛出了异常
第二次进来的时候handle的类型为com.example.backend.controller.UserController#getBrowseDetail
这次flag为true,且StpUtils.checkLogin无异常
第三次进来的时候handle的类型为com.example.backend.controller.UserController#getResponseDetail
这次flag为true,且StpUtils.checkLogin无异常
@Overridepublic void addInterceptors(InterceptorRegistry registry){log.info("注册 SaToken 拦截器...");registry.addInterceptor(new SaInterceptor(handle->{ Boolean flag = StpUtil.isLogin();StpUtil.checkLogin();})).addPathPatterns("/question/check", "/user/**", "/chatAI/cancel").excludePathPatterns("/login", "/register"); // 根据需要排除某些路径}
从你的描述来看,问题的根本原因在于 CORS 预检请求(Preflight Request) 的存在。这是浏览器在发送跨域请求时的一种安全机制,尤其是当你使用 PUT
、DELETE
、OPTIONS
等方法,或者带有自定义头部时,浏览器会先发送一个 OPTIONS
请求作为预检请求,以确认服务器是否允许实际的请求。
问题分析
-
第一次请求:
AbstractHandleMapping$PreFlightHttpRequestHandler
• 这是浏览器发送的 CORS 预检请求,类型为OPTIONS
。
• 预检请求的目的是检查服务器是否允许跨域请求。
• 由于这是一个OPTIONS
请求,Spring MVC 会由AbstractHandleMapping$PreFlightHttpRequestHandler
处理,而不是你的控制器方法。
• 在拦截器中,StpUtil.isLogin()
和StpUtil.checkLogin()
会被调用,但由于预检请求没有携带用户的登录信息(如 Token),因此会抛出异常。 -
第二次请求:
UserController#getBrowseDetail
• 这是实际的跨域请求,浏览器在收到预检请求的允许响应后,会发送真实的请求(如GET
或POST
)。
• 此时,用户的登录信息(如 Token)会被正确传递,StpUtil.isLogin()
和StpUtil.checkLogin()
可以正常工作。 -
第三次请求:
UserController#getResponseDetail
• 如果你同时触发了多个请求(例如前端代码中有多个并发请求),可能会看到类似的日志。
• 每个请求都会经过拦截器,但由于它们是真实的请求,StpUtil.checkLogin()
不会抛出异常。
解决方案
为了避免拦截器对 CORS 预检请求的处理,可以在拦截器中添加对 OPTIONS
请求的特殊处理逻辑,直接放行 OPTIONS
请求。
修改拦截器代码
@Override
public void addInterceptors(InterceptorRegistry registry) {log.info("注册 SaToken 拦截器...");registry.addInterceptor(new SaInterceptor(handle -> {// 如果是 OPTIONS 请求,直接放行if ("OPTIONS".equalsIgnoreCase(handle.getRequest().getMethod())) {return;}// 检查用户是否登录Boolean flag = StpUtil.isLogin();StpUtil.checkLogin(); // 如果未登录,会抛出异常})).addPathPatterns("")// 这里写你的自己的.excludePathPatterns(""); //这里写你自己的 根据需要排除某些路径
}
关键点
• handle.getRequest().getMethod()
:获取当前请求的 HTTP 方法。
• "OPTIONS".equalsIgnoreCase(...)
:判断是否为 OPTIONS
请求。
• 直接放行:如果是 OPTIONS
请求,直接返回,不执行后续的登录检查逻辑。
进一步优化
如果你希望更优雅地处理 CORS 预检请求,可以结合 Spring 的 CorsConfiguration
来全局配置 CORS,而不是依赖拦截器来处理。
添加 CORS 配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**") // 允许跨域的路径.allowedOrigins("*") // 允许的来源(可以改为具体的域名).allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法.allowedHeaders("*") // 允许的请求头.allowCredentials(true) // 是否允许发送凭证(如 Cookie、Token).maxAge(3600); // 预检请求的有效期(秒)}
}
优点
• 通过 CorsConfiguration
处理跨域问题,避免拦截器对 OPTIONS
请求的干扰。
• 更加清晰和规范地管理跨域请求。
总结
• 问题原因:CORS 预检请求(OPTIONS
方法)触发了拦截器的逻辑,导致异常。
• 解决方案:
- 在拦截器中添加对
OPTIONS
请求的放行逻辑。 - 使用 Spring 的
CorsConfiguration
全局配置 CORS,避免拦截器处理OPTIONS
请求。
• 推荐方案:使用CorsConfiguration
,因为它更符合 Spring 的设计理念,并且可以更好地管理跨域问题。