关于解决跨域请求携带Cookie失败的问题

场景

前后端分离项目,前端用Vue,后端用SpringBoot

登录后用Cookie存储token,但是每次请求都提示未登录,一看是Cookie没带过去

前端代码是这样的:

1
2
3
4
5
6
axios.post('/api/login', {
username: 'admin',
password: '123456'
}, {
withCredentials: true // 允许携带Cookie
})

后端也配置了跨域:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}

但是请求的时候,Cookie还是没带过去

原因分析

查了一下文档,发现当allowCredentials(true)的时候,allowedOrigins不能设置为"*",必须指定具体的域名

而且Chrome还要求,当携带Cookie的时候,Access-Control-Allow-Origin必须是请求的完整域名,不能是通配符

解决方案

方案一:指定具体域名(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 这里要指定完整的域名,不能用*
.allowedOriginPatterns("http://localhost:8080", "https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

注意这里用的是allowedOriginPatterns而不是allowedOrigins,因为Spring 5.3之后推荐用allowedOriginPatterns

方案二:使用CorsFilter

如果觉得上面的配置不生效,可以用CorsFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();

// 允许携带Cookie
config.setAllowCredentials(true);

// 允许的域名
config.addAllowedOriginPattern("http://localhost:8080");
config.addAllowedOriginPattern("https://example.com");

// 允许的所有请求头
config.addAllowedHeader("*");

// 允许的所有请求方法
config.addAllowedMethod("*");

// 暴露的响应头
config.addExposedHeader("Content-Disposition");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsFilter(source);
}
}

方案三:使用注解

如果是针对某个Controller或某个方法,可以直接用注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api")
@CrossOrigin(
origins = "http://localhost:8080",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "*",
allowCredentials = "true"
)
public class UserController {

@GetMapping("/user/info")
public User getUserInfo() {
// ...
}
}

方案四:统一使用网关

如果项目用了微服务和网关,可以在网关统一配置跨域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600

但是要注意,网关配置了跨域之后,各个微服务就不要再配置了,否则会有问题

前端配置

前端也需要配置:

1
2
3
4
5
6
7
// axios配置
axios.defaults.withCredentials = true;

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

SameSite属性

还有一个坑,就是Chrome从80版本开始,Cookie的SameSite属性默认是Lax,这可能导致跨域Cookie无法发送

可以在后端设置Cookie的时候指定SameSite属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class LoginController {

@PostMapping("/login")
public ResponseResult login(HttpServletResponse response, @RequestBody User user) {
// 验证用户名密码
// ...

// 设置Cookie
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
// 允许跨域携带
cookie.setSameSite(None.name());
// 如果设置了None,必须同时设置Secure
cookie.setSecure(true);

response.addCookie(cookie);

return ResponseResult.success();
}
}

但是要注意,SameSite=None必须配合Secure使用,也就是说只能通过HTTPS访问

如果是开发环境用的是HTTP,可以考虑:

  1. 前端和后端部署在同一个域名下
  2. 或者用SameSite=Lax,这样在某些情况下还是可以携带Cookie的

完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 不要用allowedOrigins("*"),用allowedOriginPatterns
.allowedOriginPatterns(
"http://localhost:8080",
"http://localhost:8081",
"https://example.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Content-Disposition", "token")
.allowCredentials(true)
.maxAge(3600);
}
}

常见问题

OPTIONS请求

浏览器在发送跨域请求之前,会先发一个OPTIONS预检请求

如果OPTIONS请求失败了,真正的请求就不会发送

所以要确保OPTIONS请求能通过:

1
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")

SpringSecurity拦截

如果项目用了SpringSecurity,要注意Security过滤器可能在CORS之前执行,导致跨域配置失效

需要在Security配置中启用CORS:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors() // 启用CORS
.and()
// ... 其他配置
return http.build();
}
}

总结

跨域问题看起来简单,但实际配置的时候还是有很多坑的

关键点:

  1. allowCredentials(true)时不能用allowedOrigins("*")
  2. 推荐用allowedOriginPatterns代替allowedOrigins
  3. 前端要设置withCredentials=true
  4. 注意Cookie的SameSite属性
  5. 如果用了SpringSecurity,要确保Security配置也支持CORS

暂时就先记录这么多