前言 跨域介绍 什么是跨域
:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一项不同,都属于跨域造成的原因
:由于浏览器的同源策略,即 A 网站只能访问 A 网站的内容,不能访问 B 网站的内容特别注意
:跨域问题只存在于浏览器,也就是说当前端页面访问后端的接口时,返回值是有的,只是服务器没有在请求头指定跨域的信息,所以浏览器自动把返回值给” 屏蔽了”解决跨域
:经过上面的了解,可以得出几个解决跨域的方法(这里暂不考虑前端的实现方案),一是服务端指定跨域信息,二是在 Web 页面与后端服务之间加一层服务来指定跨域信息,比如代理服务 Nginx跨域解决方案 方案一 使用 Nginx 等代理服务器,将不同的应用部署为同一域。
方案二 添加 HTTP 响应头,配置当次请求允许跨域。
Access-Control-Allow-Origin
:支持哪些来源的请求跨域Access-Control-Allow-Methods
:支持哪些方法跨域Access-Control-Allow-Credentials
:跨域请求默认不包含 Cookie,设置为 true
则可以包含 CookieAccess-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" , "*" ); 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" ); rep.setHeader("Access-Control-Allow-Credentials" , "true" ); 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
注解的底层代码并不是基于拦截器或者过滤器来实现的。
参考博客