Sa-Token解决跨域问题的三种实践方案
1. 为什么需要处理跨域问题?
跨域问题源于浏览器的同源策略(Same-Origin Policy),这是现代浏览器最基本的安全机制之一。简单来说,当你的前端页面(比如运行在http://localhost:8080)尝试通过AJAX请求后端API(比如http://api.example.com)时,浏览器会阻止这种请求,除非后端明确允许。
在实际开发中,前后端分离架构已经成为主流,前端可能部署在CDN或独立域名下,后端API则有专门的域名。这种情况下,跨域问题几乎不可避免。我曾接手过一个电商项目,前端使用Vue.js部署在static.shop.com,而后端API在api.shop.com,就因为没处理好CORS导致支付功能完全无法使用。
提示:即使前端和后端在同一台服务器,如果端口不同(如前端3000端口,后端8080端口)也会触发跨域限制。
2. Sa-Token全局过滤器基础配置
2.1 添加Sa-Token依赖
首先需要在项目中引入Sa-Token核心库。以Maven项目为例:
<dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.45.0</version> </dependency>我建议始终使用最新稳定版,因为Sa-Token团队会持续修复CORS相关的边界问题。曾经有个项目使用了较旧的1.38.0版本,在处理OPTIONS预检请求时会出现奇怪的拦截行为。
2.2 创建基础过滤器类
创建一个继承自SaServletFilter的全局过滤器:
@Configuration public class SaTokenFilter implements SaServletFilter { @Override public void addExcludes(String... paths) { // 不需要过滤的路径 } @Override public void setAuth(Object handler) { // 认证逻辑 } @Override public void setBeforeAuth(Object handler) { // 前置处理 } }这个基础结构是三种解决方案的共同起点。注意setBeforeAuth方法,这是我们后续处理CORS的关键切入点。
3. 方案一:纯Sa-Token过滤器实现
3.1 核心代码实现
在setBeforeAuth方法中添加CORS处理逻辑:
@Override public void setBeforeAuth(Object handler) { HttpServletRequest request = (HttpServletRequest) handler; HttpServletResponse response = (HttpServletResponse) handler; // 设置允许的源 response.setHeader("Access-Control-Allow-Origin", "*"); // 允许的请求方法 response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); // 预检请求缓存时间 response.setHeader("Access-Control-Max-Age", "3600"); // 允许的请求头 response.setHeader("Access-Control-Allow-Headers", "x-requested-with, sa-token, Content-Type"); }3.2 实际项目中的坑点
星号(*)限制:虽然设置为
*很方便,但在需要传递cookie时(比如单点登录场景)必须指定具体域名,且要设置Access-Control-Allow-Credentials: true。我曾在SSO集成时花了半天排查为什么cookie不生效。OPTIONS请求处理:浏览器会先发送OPTIONS预检请求。很多开发者只处理了GET/POST却漏了OPTIONS,导致PUT/DELETE等方法失败。务必确保所有方法都被列出。
Header大小写敏感:有些前端框架会自动转换header名称大小写,而后端可能区分大小写。建议统一使用小写,比如
sa-token而非Sa-Token。
4. 方案二:结合Spring的CorsRegistry
4.1 混合配置方式
如果你同时使用Spring MVC,可以结合两种方案:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*") .exposedHeaders("sa-token"); } } // 同时在SaTokenFilter中保留基础CORS设置这种组合方式特别适合大型项目,可以利用Spring更细粒度的路径控制,同时保持Sa-Token的认证逻辑。
4.2 优先级问题
Spring的CORS配置和Sa-Token过滤器的执行顺序很重要。实测发现:
- 简单请求(GET/POST):先经过Spring CORS,再到Sa-Token
- 复杂请求(PUT/DELETE等):OPTIONS请求会被Spring优先处理
这意味着如果你的Sa-Token过滤器对OPTIONS请求有特殊逻辑,可能需要调整。一个保险的做法是在SaTokenFilter中增加判断:
if ("OPTIONS".equals(request.getMethod())) { return; }5. 方案三:基于注解的精细控制
5.1 @SaCheckCors注解实现
对于需要特殊CORS规则的接口,可以创建自定义注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SaCheckCors { String[] origins() default "*"; String[] methods() default {"GET", "POST"}; String[] headers() default {}; }然后在过滤器中解析注解:
HandlerMethod handlerMethod = (HandlerMethod)handler; SaCheckCors corsConfig = handlerMethod.getMethodAnnotation(SaCheckCors.class); if (corsConfig != null) { response.setHeader("Access-Control-Allow-Origin", String.join(",", corsConfig.origins())); // 其他header设置... }5.2 实际应用场景
这种方案特别适合:
- 某些接口需要开放给特定合作伙伴域名
- 不同接口支持不同的HTTP方法
- 需要动态调整CORS策略的灰度发布场景
比如支付回调接口可能只允许来自支付平台的POST请求:
@PostMapping("/pay/callback") @SaCheckCors(origins = "https://payment-platform.com", methods = "POST") public Result payCallback() { // ... }6. 生产环境最佳实践
6.1 安全加固建议
- 不要无脑使用
*:根据项目实际需要设置允许的源,可以配置在application.yml中:
sa-token: cors: allowed-origins: - https://www.yourdomain.com - https://admin.yourdomain.com- 限制暴露的Header:只暴露必要的Header,避免泄露敏感信息:
response.setHeader("Access-Control-Expose-Headers", "sa-token, content-length");- 结合HTTPS:CORS+HTTP是不安全的组合,生产环境必须启用HTTPS。
6.2 性能优化
- 合理设置Max-Age:对于稳定的API,可以设置较长的缓存时间减少OPTIONS请求:
response.setHeader("Access-Control-Max-Age", "86400"); // 24小时- 避免重复处理:在过滤器中添加判断,已经处理过的请求不再重复设置Header:
if (response.getHeader("Access-Control-Allow-Origin") != null) { return; }- 网关层统一处理:如果项目使用微服务架构,可以考虑在API网关统一处理CORS,避免每个服务重复配置。
7. 疑难问题排查指南
7.1 常见错误现象
控制台报错但服务端日志无记录:通常是浏览器拦截了请求,根本未到达服务端。检查网络面板确认请求是否真的发出。
POST变成OPTIONS:这是正常预检机制,检查你的服务是否正确响应了OPTIONS方法。
明明配置了CORS还是报错:
- 检查是否有多个过滤器/拦截器覆盖了CORS头
- 使用curl测试确认响应头是否正确:
curl -I -X OPTIONS http://your-api.com/endpoint
7.2 调试技巧
浏览器无痕模式测试:避免缓存干扰,Chrome的隐身窗口是最干净的测试环境。
简化复现:创建一个最小化的测试接口,排除业务逻辑干扰:
@GetMapping("/test-cors") public String testCors() { return "CORS test"; }- 查看完整请求链路:使用Charles或Fiddler抓包,对比请求和响应的完整Header信息。
8. 三种方案对比与选型建议
8.1 方案对比表
| 特性 | 纯Sa-Token方案 | Spring混合方案 | 注解方案 |
|---|---|---|---|
| 实现复杂度 | 低 | 中 | 高 |
| 灵活性 | 一般 | 较好 | 最好 |
| 性能影响 | 小 | 较小 | 中等 |
| 适合场景 | 简单项目 | Spring项目 | 需要精细控制 |
| 维护成本 | 低 | 中 | 较高 |
8.2 个人经验建议
根据我参与过的十几个项目经验:
初创项目/快速原型:选择方案一,简单直接,10分钟搞定。
标准Spring Boot应用:方案二最平衡,既利用Spring生态,又保持Sa-Token特性。
企业级复杂系统:特别是需要对接多第三方系统时,方案三的精细控制必不可少。
一个实际案例:我们有个金融项目需要对接5个外部支付渠道,每个渠道的CORS要求都不同,最终采用方案三为每个渠道接口单独配置,大大降低了维护难度。
