场景
前后端分离项目,前端用Vue,后端用SpringBoot
登录后用Cookie存储token,但是每次请求都提示未登录,一看是Cookie没带过去
前端代码是这样的:
1 2 3 4 5 6
| axios.post('/api/login', { username: 'admin', password: '123456' }, { withCredentials: true })
|
后端也配置了跨域:
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();
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.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 = new Cookie("token", token); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setSameSite(None.name()); cookie.setSecure(true);
response.addCookie(cookie);
return ResponseResult.success(); } }
|
但是要注意,SameSite=None必须配合Secure使用,也就是说只能通过HTTPS访问
如果是开发环境用的是HTTP,可以考虑:
- 前端和后端部署在同一个域名下
- 或者用
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("/**") .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() .and() return http.build(); } }
|
总结
跨域问题看起来简单,但实际配置的时候还是有很多坑的
关键点:
allowCredentials(true)时不能用allowedOrigins("*")
- 推荐用
allowedOriginPatterns代替allowedOrigins
- 前端要设置
withCredentials=true
- 注意Cookie的
SameSite属性
- 如果用了SpringSecurity,要确保Security配置也支持CORS
暂时就先记录这么多