Spring 与 SpringBoot 配置跨域的几种方式

前言

跨域介绍

  • 什么是跨域:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一项不同,都属于跨域
  • 造成的原因:由于浏览器的同源策略,即 A 网站只能访问 A 网站的内容,不能访问 B 网站的内容
  • 特别注意:跨域问题只存在于浏览器,也就是说当前端页面访问后端的接口时,返回值是有的,只是服务器没有在请求头指定跨域的信息,所以浏览器自动把返回值给” 屏蔽了”
  • 解决跨域:经过上面的了解,可以得出几个解决跨域的方法(这里暂不考虑前端的实现方案),一是服务端指定跨域信息,二是在 Web 页面与后端服务之间加一层服务来指定跨域信息,比如代理服务 Nginx

提示

更多关于跨域的详细介绍内容,可以看 这里

跨域解决方案

方案一

使用 Nginx 等代理服务器,将不同的应用部署为同一域。

方案二

添加 HTTP 响应头,配置当次请求允许跨域。

  • Access-Control-Allow-Origin:支持哪些来源的请求跨域
  • Access-Control-Allow-Methods:支持哪些方法跨域
  • Access-Control-Allow-Credentials:跨域请求默认不包含 Cookie,设置为 true 则可以包含 Cookie
  • Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该字段的值超过了最大有效时间,将不会生效
  • Access-Control-Expose-Headers:跨域请求暴露的字段。发出跨域请求时,XMLHttpRequest 对象的 getResponseHeader () 方法默认只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定

Spring 配置跨域

使用注解实现跨域

特别注意:Spring 的版本要在 4.2 或以上版本才支持使用 @CrossOrigin 注解来控制跨域,使用注解的方式优势在于比较容易细粒度(局部)地实现跨域控制

在 Controller 类中配置跨域,可以使用注解 @CrossOrigin,该注解支持写在类或者方法上,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {

}

}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.web.bind.annotation.*;
import static org.springframework.web.bind.annotation.RequestMethod.*;

@RestController
@RequestMapping("/account")
@CrossOrigin(origins = {"http://example.com"}, maxAge = 3600, allowedHeaders = {"Origin", "X-Requested-With", "Content-Type", "Accept", "token"}, methods = {GET, POST, PUT, OPTIONS, DELETE, PATCH})
public class AccountController {

@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {

}

}

@CrossOrigin 注解中的参数说明如下:

  • origins:允许来源域名的列表,不设置确切值时默认支持所有域名跨域访问
  • methods: 跨域请求中支持的 HTTP 请求的类型(GET、POST、DELETE …),不指定确切值时默认与 Controller 方法中的 methods 字段保持一致
  • maxAge:跨域预检请求的有效期(单位为秒),目的是减少浏览器预检 / 响应的请求数量,默认值是 1800秒;设置了该值后,浏览器将在设置值的时间段内对该跨域请求不再发起预检请求
  • exposedHeaders:跨域请求的请求头中允许携带除 Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma 这六个基本字段之外的其他字段信息
  • allowedHeaders:允许的请求头中的字段类型,不设置确切值时默认支持所有的 Header 字段(Cache-Controller、Content-Language、Content-Type、Expires、Last-Modified、Pragma)跨域访问
  • allowCredentials:浏览器是否将本域名下的 Cookie 信息携带至跨域服务器中,若设置为携带 Cookie 至跨域服务器中,要实现 Cookie 共享还需要前端在 AJAX 请求中打开 withCredentials 属性

SpringMVC 还支持同时使用类和方法级别的跨域配置,此时 SpringMVC 会合并两个注解属性以创建合并后的跨域配置

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/account")
@CrossOrigin(maxAge = 3600)
public class AccountController {

@CrossOrigin(origins = {"http://example.com"})
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {

}

}

如果在 Spring 项目里使用了 Spring Security,请确保 Spring Security 在安全级别启用 CORS,并允许它利用 Spring MVC 级别的配置定义

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}

使用拦截器实现跨域

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
29
30
31
32
33
34
35
36
37
38
39
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.addHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,token");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
});
}

}

由于请求头中自定义的字段是不允许跨域的,所以需要指定允许跨域的自定义 Header,上述的代码段如下:

1
response.addHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With,Accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,token");

使用过滤器实现跨域

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Component
@WebFilter(urlPatterns = {"/*"}, filterName = "corsFilter")
public class CorsFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse rep = (HttpServletResponse) response;
HttpSession session = req.getSession();

// 设置允许跨域的来源域名
rep.setHeader("Access-Control-Allow-Origin", "*");
// 设置允许跨域请求中支持的HTTP请求类型
rep.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH");
// 设置跨域预检请求的有效期(秒)
rep.setHeader("Access-Control-Max-Age", "3600");
// 设置允许跨域的请求头字段
rep.setHeader("Access-Control-Allow-Headers", "token, Origin, X-Requested-With, Content-Type, Accept");
// 设置允许将本站域名下的Cookie信息携带至跨域服务器
rep.setHeader("Access-Control-Allow-Credentials", "true");
// 将获取到的SessionId通过Cookie返回给前端
// rep.addCookie(new Cookie("JSSESIONID", session.getId()));
chain.doFilter(req, rep);
}

@Override
public void init(FilterConfig arg0) throws ServletException {

}

@Override
public void destroy() {

}

}

SpringBoot 配置跨域

特别注意:上述介绍的 Spring 使用注解、拦截器、过滤器控制跨域的方式,同样适用于 SpringBoot 项目

SpringBoot 1.5 版本

在 SpringBoot 1.5 版本里,可以继承 WebMvcConfigurerAdapter 类并实现 addCorsMappings() 抽象方法

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*")
.allowCredentials(true);
}
}

SpringBoot 2.0 版本

在 SpringBoot 2.0 版本里,可以实现 WebMvcConfigurer 接口并实现 addCorsMappings() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("Content-Type", "X-Requested-With", "Accept,Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "token")
.allowedMethods("*")
.allowedOrigins("*")
.allowCredentials(true);
}

}

Gateway 配置跨域

由于 Spring Cloud Gateway 是基于 WebFlux 开发的,因此上述配置跨域的方式都不适用于 Gateway,具体可参考以下配置类:

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();

// 配置跨域
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://127.0.0.1:8080");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);

corsSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(corsSource);
}

}

提示

1、如果 AllowCredentials 设置为 false,则 AllowedOrigin 可以指定为 *,表示所有来源的请求都允许跨域
2、如果 AllowCredentials 设置为 true,则 AllowedOrigin 不能指定为 *,必须明确指定哪些来源的请求允许跨域

扩展说明

Nginx 配置跨域

Access-Control-Max-Age 参数

浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域 HTTP 请求(比如异步请求 GET、POST、PUT、DELETE、OPTIONS 等等),所以浏览器会向所请求的服务器发起两次请求;第一次是浏览器使用 OPTIONS 方法发起一个预检请求,第二次才是真正的请求;第一次的预检请求获知服务器是否允许该跨域请求:如果允许,才发起第二次真实的请求;如果不允许,则拦截第二次请求。Access-Control-Max-Age:3600(单位为秒,有效期为 1 小时)表示该预检请求在客户端 1 小时后过期,即 1 小时内发送普通请求就不会再伴随着发送预检请求,这样可以减少对服务器的压力,但是时间也不宜设置太大,尤其是项目频繁发布版本的阶段,同时又修改了 Cors 配置的场景。

  • resp.addHeader("Access-Control-Max-Age", "0"):表示每次请求都发起预检请求,也就是说每次都发送两次请求
  • resp.addHeader("Access-Control-Max-Age", "1800"):表示每隔 30 分钟才发起一次预检请求

Access-Control-Allow-Credentials 参数

如果服务器端设置了 Access-Control-Allow-Credentials: true,同时服务器端还设置了 Access-Control-Allow-Origin: *,那就意味将 Cookie 暴露给了所有的网站。举个例子,假设当前是 A 网站,并且在 Cookie 里写入了身份凭证,用户同时打开了 B 网站,那么 B 网站给 A 网站的服务器发的所有请求都是以 A 用户的身份进行的,这将导致 CSRF 系统安全问题。

常见问题

@CrossOrigin 注解不生效

  • 1.Spring 的版本要在 4.2 或以上版本才支持 @CrossOrigin 注解

  • 2. 并非 @CrossOrigin 没有解决跨域的问题,而是不正确的请求导致无法得到预期的响应,最终使浏览器端提示跨域错误,此时建议检查 HTTP 请求的响应状态码

  • 3. 在 Controller 类上方添加 @CrossOrigin 注解后,仍然出现跨域问题,解决方案之一就是在方法上的 @RequestMapping 注解中指定 GET、POST 等方式,示例代码如下:

1
2
3
4
5
6
7
8
9
@CrossOrigin
@RestController
public class AccountController {

@RequestMapping(method = RequestMethod.GET)
public String add() {

}
}

注解方式与过滤器方式的适用场景

过滤器 / 拦截器方式适合于大范围的跨域控制,比如某个 Controller 类的所有方法全部支持某个或几个具体的域名跨域访问的场景。而注解方式的优势在于细粒度的跨域控制,比如一个 Controller 类中 methodA 支持域名 originA 跨域访问,methodB 支持域名 originB 跨域访问的情况,当然过滤器 / 拦截器方式也能实现,但使用注解的方式能轻松很多,尤其是上述情况比较多的场景。值得一提的是,@CrossOrigin 注解的底层代码并不是基于拦截器或者过滤器来实现的。

参考博客