前后端分离项目,跨域是最常见的问题之一。浏览器报一堆红色的CORS错误,很多新人不知道怎么处理。这节讲前端和后端两种方案,彻底解决跨域。
一、什么是跨域
同源策略
浏览器有个安全策略:A网站不能直接访问B网站的接口。
"同源" = 协议相同 + 域名相同 + 端口相同。三者只要有一个不同,就是跨域。
前端地址:http://localhost:3000
后端地址:http://localhost:8080
端口不同 → 跨域!
跨域报错长什么样
F12 → Console面板:
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请求先发到前端服务器,前端服务器再转发到后端。
浏览器 → 前端服务器(3000) → 后端服务器(8080)
↑ 浏览器看到的请求 ↑ 服务器之间的请求,不跨域
浏览器只和前端服务器通信,不存在跨域问题。
Vite配置
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, // 改变请求头中的Origin
// pathRewrite: { '^/api': '' } // 如果后端没有/api前缀,去掉
}
}
}
})
前端代码里用相对路径请求:
// 请求会发到 http://localhost:3000/api/user
// 前端服务器转发到 http://localhost:8080/api/user
axios.get('/api/user/list')
pathRewrite什么时候用:
- 后端接口路径有
/api前缀 → 不需要pathRewrite - 后端接口路径没有
/api前缀 → 需要去掉/api
// 后端路径是 /user/list(没有/api)
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' } // /api/user/list → /user/list
}
}
Vue CLI配置
// vue.config.js
module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
多个后端服务代理
如果有多个后端服务:
// 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
// 错误:写了完整地址,不经过代理
axios.get('http://localhost:8080/api/user')
// 正确:用相对路径
axios.get('/api/user')
检查2: 重启前端开发服务器
改了vite.config.js或vue.config.js后,必须重启前端服务才生效。
检查3: 确认请求路径匹配代理规则
// 代理规则是 /api
proxy: { '/api': { ... } }
// 那请求必须以 /api 开头
axios.get('/api/user/list') // ✅ 匹配
axios.get('/user/list') // ❌ 不匹配,不经过代理
三、方案2:后端CORS配置
如果前端不方便配代理,或者后端需要被多个前端调用,在后端配CORS。
方式1:全局配置(推荐)
@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类或方法上加注解:
@CrossOrigin(origins = "*") // 允许所有源
@RestController
@RequestMapping("/user")
public class UserController {
@CrossOrigin(origins = "http://localhost:3000") // 只允许指定源
@GetMapping("/list")
public Result list() { ... }
}
方式3:Filter方式
@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:
@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。
OPTIONS /api/user/list → 401 Unauthorized
后续的真实请求也不会发了。
解决:放行OPTIONS请求
方法1:拦截器里放行
@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里放行
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll() // 放行所有OPTIONS请求
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
}
方法3:Filter里处理
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(200);
return; // 直接返回,不走后续逻辑
}
五、带Cookie的跨域
如果前端请求需要携带Cookie(比如session),需要额外配置。
前端配置
// axios
axios.defaults.withCredentials = true
// 或单个请求
axios.get('/api/user', { withCredentials: true })
后端配置
// 允许携带Cookie
.allowCredentials(true)
注意: allowCredentials(true)时,allowedOrigin不能是*,必须指定具体域名:
// 错误:allowCredentials(true)时不能用*
.allowedOrigins("*")
.allowCredentials(true)
// 正确:指定具体域名
.allowedOrigins("http://localhost:3000")
.allowCredentials(true)
// 或者用 allowedOriginPatterns
.allowedOriginPatterns("*")
.allowCredentials(true)
六、生产环境的跨域处理
用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,指定允许的域名:
registry.addMapping("/**")
.allowedOrigins("https://www.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
七、常见跨域问题
问题1:配了CORS还是跨域
检查:
- CORS配置类有没有被Spring扫描到(确认有
@Configuration注解) - 有没有被其他Filter/Interceptor拦截
- 是否用了Spring Security但没开
http.cors()
问题2:有些接口跨域有些不跨域
可能是拦截器只拦截了部分路径,OPTIONS请求在未被CORS处理的路径上被拦了。
确认CORS配置的addMapping("/**")覆盖了所有路径。
问题3:跨域配置在生产环境不生效
可能是Nginx或网关层覆盖了CORS头。检查Nginx配置有没有设置CORS头。
# Nginx也配了CORS头,和后端重复了
add_header Access-Control-Allow-Origin *;
# 解决:要么Nginx配,要么后端配,不要两边都配
问题4:文件上传跨域
文件上传用的是multipart/form-data,可能触发预检请求。
确保CORS配置允许multipart/form-data:
.allowedHeaders("*") // 允许所有头
问题5:WebSocket跨域
WebSocket的跨域处理和HTTP不一样。
// Spring WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws")
.setAllowedOrigins("*"); // 允许跨域
}
}
八、排查跨域的步骤
跨域报错
│
├─ 是开发环境吗?
│ ├─ 是 → 配前端Proxy(最简单)
│ └─ 否 ↓
│
├─ 后端配了CORS吗?
│ ├─ 没配 → 加CORS配置
│ └─ 配了 ↓
│
├─ OPTIONS预检请求被拦了吗?
│ ├─ 被拦 → 放行OPTIONS
│ └─ 没拦 ↓
│
├─ 带Cookie吗?
│ ├─ 带 → 前端withCredentials + 后端allowCredentials
│ └─ 不带 ↓
│
├─ 用了Spring Security吗?
│ ├─ 用了 → http.cors()开启
│ └─ 没用 ↓
│
└─ Nginx有没有重复配CORS? → 去掉一边
小结
| 方案 | 适用场景 | 优点 |
|---|---|---|
| 前端Proxy | 开发环境 | 最简单,只改前端 |
| 后端CORS | 后端被多个前端调用 | 统一配置 |
| Nginx反向代理 | 生产环境 | 前后端同域,无跨域 |
开发环境优先用前端Proxy,生产环境用Nginx统一入口。