十一、跨域
简介
跨域问题实际应用开发中的一个非常常见的需求,在 Spring 框架中对于跨域问题的处理方案有好几种,引入了 Spring Security 之后,跨域问题的处理方案又增加了
什么是 CORS
CORS(Cross-Origin Resource-Sharing)是由 W3C 制定的一种跨域资源共享技术标准,其目的就是为了解决前端的跨域请求。在 JavaEE 开发中,最常见的前端跨域请求解决方案是早期的 JSONP,但是 JSONP 只支持GET
请求,这是一个很大的缺陷,而 CORS 则支持多种 HTTP 请求方法,也是目前主流的跨域解决方案
CORS 中新增了一组 HTTP 请求头字段,通过这些字段,服务器告诉浏览器,哪些网站通过浏览器有权限访问哪些资源。同时规定,对哪些可能修改服务器数据的 HTTP 请求方法(如 GET 以外的 HTTP 请求等),浏览器必须首先使用OPTIONS
方法发起一个预检请求(Pre inspection request),预检请求的目的是查看服务端是否支持即将发起的跨域请求,如果服务端允许,才发送实际的 HTTP 请求。在预检请求的返回中,服务器也可以通知客户端,是否需要携带身份凭证(如 Cookie、HTTP 认证信息等)
CORS:同源/同域 = 协议 + 主机 + 端口
简单请求
GET 请求为例,如果需要发起一个跨域请求,请求头如下
Host: locahost:8080
Origin: http://locahost:8081
Referer: http://locahost:8081/index.html
如果服务端(8080)支持该跨域请求,那么返回响应头中将包含如下字段
Access-Control-Allow-Origin: http://locahost:8081
Access-Control-Allow-Origin 字段用来告诉浏览器可以访问该资源的域,当浏览器收到这样的响应头信息之后,提取出Access-Control-Allow-Origin
字段中的值,发现该值包含当前页面所在域,就知道这个域是被允许的,因此就不再对前端的跨域请求进行限制。这个属于简单请求,即不需要进行预检请求的跨域
非简单请求
对于一些非简单请求,会首先发送一个与检验请求。预检请求类似下面这样
OPTIONS /put HTTP/1.1
Host: locahost:8080
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Origin: http://localhost:8081
Referer: http://localhost:8081/index.html
请求方法是OPTIONS
,请求头 Origin 就告诉服务端当前页面所在域,请求头Access-Control-Request-Methods
告诉服务器即将发起的跨域请求所使用的方法。服务端对此进行判断,如果允许即将发起的跨域请求,则会给出如下响应
HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:8081
Access-Control-Request-Methods: PUT
Access-Control-Max-Age: 3600
- Access-Control-Request-Methods:表示允许的跨域方法
- Access-Control-Max-Age:表示预检请求的有效期,单位为秒。在有效期内如果发起该跨域请求,则不用再次发起预检请求。预检请求结束后,接下来就会发起一个真正的跨域请求,跨域请求和前面简单请求跨域步骤类似
11.1 Spring 跨域解决方案
方式一:@CrossOrigin
Spring 中第一种处理跨域的方式是通过@CrossOrigin
注解来标记支持跨域,该注解可以添加在方法上,也可以添加在 Controller 上。当添加在 Controller 上时,表示 Controller 中的所有接口都支持跨域。具体配置如下
@RestController
public class TestController {
@GetMapping("/cors")
@CrossOrigin(origins = "http://127.0.0.1:8081")
public String testCors() {
return "Hello Cors!";
}
}
@CrossOrigin
注解个属性含义如下
- allowCredentials:浏览器是否应当发送凭证信息,如 Cookie(字符串)
- allowedHeaders:请求被允许的请求头字段,
*
表示所有字段(字符串数组) - exposedHeaders:哪些响应头可以作为相应的一部分暴露出来(字符串数组)
- 【注】这里可以一一列举,通配符
*
在这里是无效的
- 【注】这里可以一一列举,通配符
- maxAge:预检请求的有效期,有效期内不必再次发送预检请求,默认是
1800
秒 - methods:允许的请求方法,
*
表示允许所有方法(请求方法数组) - origins:允许的域,
*
表示允许所有域(字符串数组)
方式二:addCrosMapping
@CrossOrigin 注解需要添加在不同的 Controller 上。所以还有一种全局配置方法,就是通过重写 WebMvcConfigurer#addCorsMappings() 方法实现,具体配置如下
package com.vinjcent.config;
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 WebMvcConfiguration implements WebMvcConfigurer {
// 用来全局处理跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 对哪些请求进行跨域
.allowedMethods("*")
.allowedOrigins("*")
.allowedHeaders("*")
.maxAge(3600);
}
}
方法三:CorsFilter
CorsFilter 是 Spring Web 中提供的一个处理跨域的过滤器,开发者也可以通过该过滤器处理跨域
package com.vinjcent.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
@Configuration
public class WebMvcFilter {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
// 1.定义需要注册的过滤器bean
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
// 2.定义跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
// 3.定义过滤器生效的url配置
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(-1); // 一般是0、1,代表过滤器顺序等级,-1代表优先
return registrationBean;
}
}
11.2 Spring Security 跨域解决方案
原理分析
当我们为项目添加了 Spring Security 依赖之后,发现上面三种跨域方式有的失效,有的则可以继续使用
通过@CorsOrigin
注解或者重写 addCorsMappings 方法配置跨域,统统失效,通过 CorsFilter 配置的跨域,有没有失效则要看过滤器的优先级。如果过滤器优先级高于 Spring Security 过滤器,即先于 Spring Security 过滤器执行,则 CorsFilter所配置的跨域处理依然有效;如果过滤器优先级低于 Spring Security 过滤器,则 CorsFilter 所配置的跨域处理就会失效
为了理清楚这个问题,我们先简略了解一下 Filter、DispatcherServlet 以及 Interceptor 执行顺序
理解清楚了执行顺序之后,我们再来看跨域请求过程。由于非简单请求首先发送一个预检请求(Pre inspection request),而预检请求并不会携带认证信息,所以预检请求就有被 Spring Security 拦截的可能。因此通过@CorsOrigin
注解或者重写 addCorsMappings() 方法配置跨域就会失效。如果使用 CorsFilter 配置的跨域,只要过滤器优先级高于 Spring Security 过滤器就不会有问题,反之同样会出现问题
解决方案
Spring Security 中也提供了更专业的方式来解决预检请求所面临的问题,如
package com.vinjcent.config.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
/**
* 自定义 Security 配置
* 在 springboot 2.7.x 后,WebSecurityConfigurerAdapter 配置不再存在
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求都需要认证
http.authorizeRequests()
.anyRequest()
.authenticated();
// 开启登录请求
http.formLogin();
// 解决跨域请求
http.cors()
.configurationSource(configurationSource());
// 关闭 csrf 跨域请求伪造攻击
http.csrf()
.disable();
}
// 跨域请求配置
public CorsConfigurationSource configurationSource() {
// 1.定义跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
// 2.定义过滤器生效的url配置
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}