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

前言

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

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);
}

}

扩展说明

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 注解的底层代码并不是基于拦截器或者过滤器来实现的。

参考博客