發了 /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 的設計理念,并且可以更好地管理跨域問題。