💜 想真正擅长编程? 从一门课程开始,100% 免费

3-6、跨域问题解决

前后端分离项目,跨域是最常见的问题之一。浏览器报一堆红色的CORS错误,很多新人不知道怎么处理。这节讲前端和后端两种方案,彻底解决跨域。


一、什么是跨域

同源策略

浏览器有个安全策略:A网站不能直接访问B网站的接口。

"同源" = 协议相同 + 域名相同 + 端口相同。三者只要有一个不同,就是跨域。

TEXT
前端地址:http://localhost:3000
后端地址:http://localhost:8080

端口不同 → 跨域!

跨域报错长什么样

F12 → Console面板:

TEXT
Access to XMLHttpRequest at 'http://localhost:8080/api/user' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

F12 → Network面板,请求状态码可能是红色,Response Headers里没有Access-Control-Allow-Origin。

什么时候不会跨域

  • 前后端部署在同一个域名同一端口(比如用Nginx统一入口)
  • 前后端在同一台机器上且端口相同
  • 非浏览器环境(Postman、curl不发跨域请求)

所以用Postman测接口没问题,但前端浏览器访问就跨域了。


二、方案1:前端代理(推荐)

原理

前端启动一个本地开发服务器,所有API请求先发到前端服务器,前端服务器再转发到后端。

TEXT
浏览器 → 前端服务器(3000) → 后端服务器(8080)
         ↑ 浏览器看到的请求     ↑ 服务器之间的请求,不跨域

浏览器只和前端服务器通信,不存在跨域问题。

Vite配置

JavaScript
// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',  // 后端地址
        changeOrigin: true,               // 改变请求头中的Origin
        // pathRewrite: { '^/api': '' }    // 如果后端没有/api前缀,去掉
      }
    }
  }
})

前端代码里用相对路径请求:

JavaScript
// 请求会发到 http://localhost:3000/api/user
// 前端服务器转发到 http://localhost:8080/api/user
axios.get('/api/user/list')

pathRewrite什么时候用:

  • 后端接口路径有/api前缀 → 不需要pathRewrite
  • 后端接口路径没有/api前缀 → 需要去掉/api
JavaScript
// 后端路径是 /user/list(没有/api)
proxy: {
  '/api': {
    target: 'http://localhost:8080',
    changeOrigin: true,
    pathRewrite: { '^/api': '' }  // /api/user/list → /user/list
  }
}

Vue CLI配置

JavaScript
// vue.config.js
module.exports = {
  devServer: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
}

多个后端服务代理

如果有多个后端服务:

JavaScript
// vite.config.js
proxy: {
  '/api': {
    target: 'http://localhost:8080',
    changeOrigin: true
  },
  '/admin': {
    target: 'http://localhost:8081',
    changeOrigin: true
  },
  '/ws': {
    target: 'ws://localhost:8080',  // WebSocket
    ws: true,
    changeOrigin: true
  }
}

配代理后还是跨域?

检查1: 前端代码里是不是写了完整URL

JavaScript
// 错误:写了完整地址,不经过代理
axios.get('http://localhost:8080/api/user')

// 正确:用相对路径
axios.get('/api/user')

检查2: 重启前端开发服务器

改了vite.config.js或vue.config.js后,必须重启前端服务才生效。

检查3: 确认请求路径匹配代理规则

JavaScript
// 代理规则是 /api
proxy: { '/api': { ... } }

// 那请求必须以 /api 开头
axios.get('/api/user/list')  // ✅ 匹配
axios.get('/user/list')     // ❌ 不匹配,不经过代理

三、方案2:后端CORS配置

如果前端不方便配代理,或者后端需要被多个前端调用,在后端配CORS。

方式1:全局配置(推荐)

Java
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                // 允许的源,*表示所有。生产环境建议指定域名
                .allowedOriginPatterns("*")
                // 允许的HTTP方法
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
                // 允许的请求头
                .allowedHeaders("*")
                // 暴露的响应头
                .exposedHeaders("*")
                // 是否允许携带Cookie
                .allowCredentials(true)
                // 预检请求缓存时间(秒)
                .maxAge(3600);
    }
}

方式2:注解方式

在Controller类或方法上加注解:

Java
@CrossOrigin(origins = "*")  // 允许所有源
@RestController
@RequestMapping("/user")
public class UserController {
    
    @CrossOrigin(origins = "http://localhost:3000")  // 只允许指定源
    @GetMapping("/list")
    public Result list() { ... }
}

方式3:Filter方式

Java
@Component
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        HttpServletRequest req = (HttpServletRequest) request;
        
        res.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        res.setHeader("Access-Control-Allow-Headers", "*");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        res.setHeader("Access-Control-Max-Age", "3600");
        
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
            res.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        chain.doFilter(request, response);
    }
}

方式4:Spring Security中配置CORS

如果项目用了Spring Security,上面几种方式可能被Security拦掉。需要在Security配置中开启CORS:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()  // 开启CORS支持
            .and()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

四、OPTIONS预检请求

什么是预检请求

浏览器在发送某些跨域请求前,会先发一个OPTIONS请求,问服务器:"我能不能用这个方法/头发请求?"

这就是预检请求(Preflight Request)。

什么时候发预检请求

满足以下任一条件就会发预检:

  • 请求方法不是GET/HEAD/POST
  • POST的Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 设置了自定义请求头(如Authorization、X-Token等)

简单说:带Token的请求一定会发预检。

预检请求被拦截的问题

后端有认证拦截器,OPTIONS请求没带Token,被拦了返回401。

TEXT
OPTIONS /api/user/list  →  401 Unauthorized

后续的真实请求也不会发了。

解决:放行OPTIONS请求

方法1:拦截器里放行

Java
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, Object handler) {
        // 放行OPTIONS预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return false;
        }
        
        // 正常的认证逻辑
        String token = request.getHeader("Authorization");
        if (token == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
}

方法2:Spring Security里放行

Java
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.cors().and()
        .authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll()  // 放行所有OPTIONS请求
        .antMatchers("/login").permitAll()
        .anyRequest().authenticated();
}

方法3:Filter里处理

Java
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
    response.setStatus(200);
    return;  // 直接返回,不走后续逻辑
}

五、带Cookie的跨域

如果前端请求需要携带Cookie(比如session),需要额外配置。

前端配置

JavaScript
// axios
axios.defaults.withCredentials = true

// 或单个请求
axios.get('/api/user', { withCredentials: true })

后端配置

Java
// 允许携带Cookie
.allowCredentials(true)

注意: allowCredentials(true)时,allowedOrigin不能是*,必须指定具体域名:

Java
// 错误:allowCredentials(true)时不能用*
.allowedOrigins("*")
.allowCredentials(true)

// 正确:指定具体域名
.allowedOrigins("http://localhost:3000")
.allowCredentials(true)

// 或者用 allowedOriginPatterns
.allowedOriginPatterns("*")
.allowCredentials(true)

六、生产环境的跨域处理

用Nginx统一入口(推荐)

生产环境一般用Nginx做反向代理,前端和后端在同一个域名下,根本不存在跨域。

nginx
server {
    listen 80;
    server_name www.example.com;

    # 前端静态资源
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # 后端API
    location /api/ {
        proxy_pass http://后端服务器地址:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

前端请求/api/user,Nginx转发到后端,浏览器看到的都是同一个域名,不跨域。

如果前后端不同域名

后端配置CORS,指定允许的域名:

Java
registry.addMapping("/**")
    .allowedOrigins("https://www.example.com")
    .allowedMethods("GET", "POST", "PUT", "DELETE")
    .allowCredentials(true);

七、常见跨域问题

问题1:配了CORS还是跨域

检查:

  1. CORS配置类有没有被Spring扫描到(确认有@Configuration注解)
  2. 有没有被其他Filter/Interceptor拦截
  3. 是否用了Spring Security但没开http.cors()

问题2:有些接口跨域有些不跨域

可能是拦截器只拦截了部分路径,OPTIONS请求在未被CORS处理的路径上被拦了。

确认CORS配置的addMapping("/**")覆盖了所有路径。

问题3:跨域配置在生产环境不生效

可能是Nginx或网关层覆盖了CORS头。检查Nginx配置有没有设置CORS头。

nginx
# Nginx也配了CORS头,和后端重复了
add_header Access-Control-Allow-Origin *;

# 解决:要么Nginx配,要么后端配,不要两边都配

问题4:文件上传跨域

文件上传用的是multipart/form-data,可能触发预检请求。

确保CORS配置允许multipart/form-data:

Java
.allowedHeaders("*")  // 允许所有头

问题5:WebSocket跨域

WebSocket的跨域处理和HTTP不一样。

Java
// Spring WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/ws")
                .setAllowedOrigins("*");  // 允许跨域
    }
}

八、排查跨域的步骤

TEXT
跨域报错
  │
  ├─ 是开发环境吗?
  │    ├─ 是 → 配前端Proxy(最简单)
  │    └─ 否 ↓
  │
  ├─ 后端配了CORS吗?
  │    ├─ 没配 → 加CORS配置
  │    └─ 配了 ↓
  │
  ├─ OPTIONS预检请求被拦了吗?
  │    ├─ 被拦 → 放行OPTIONS
  │    └─ 没拦 ↓
  │
  ├─ 带Cookie吗?
  │    ├─ 带 → 前端withCredentials + 后端allowCredentials
  │    └─ 不带 ↓
  │
  ├─ 用了Spring Security吗?
  │    ├─ 用了 → http.cors()开启
  │    └─ 没用 ↓
  │
  └─ Nginx有没有重复配CORS? → 去掉一边

小结

方案 适用场景 优点
前端Proxy 开发环境 最简单,只改前端
后端CORS 后端被多个前端调用 统一配置
Nginx反向代理 生产环境 前后端同域,无跨域

开发环境优先用前端Proxy,生产环境用Nginx统一入口。