跨域解决方案

  • ​​1、什么是跨域​​
  • ​​2、Spring Boot跨域解决方案​​
  • ​​1、服务端代码​​
  • ​​2、前端页面​​
  • ​​3、运行服务​​
  • ​​3、Spring Security跨域解决方案​​

1、什么是跨域

什么是跨域,首先可以参考我之前写的这篇文章:​​JavaWeb跨域问题及解决方案​​ ,另外我下面会做补充。

很多人误认为资源跨域时无法请求,实际上,通常情况下请求是可以正常发起的(注意,部分浏览器存在特例),后端也正常进行了处理,只是在返回时被浏览器拦截,导致响应内容不可使用。此外,我们平常所说的跨域实际上都是在讨论浏览器行为。

CORS(Cross-Origin Resource Sharing)的规范中有一组新增的HTTP首部字段,允许服务器声明其提供的资源允许哪些站点跨域使用。通常情况下,跨域请求即便在不被支持的情况下,服务器也会接收并进行处理,在CORS的规范中则避免了这个问题。浏览器首先会发起一个请求方法为OPTIONS 的预检请求,用于确认服务器是否允许跨域,只有在得到许可后才会发出实际请求。此外,预检请求还允许服务器通知浏览器跨域携带身份凭证(如cookie)。

CORS常见首部字段:

  • Access-Control-Allow-Origin:具体的站点或*,如果需要浏览器发起请求时携带凭证信息则不允许为*,如果为具体的站点则响应头的Vary字段需要携带Origin属性
  • Access-Control-Allow-Methods:仅在预检请求的响应中指定有效,用于表明服务器允许跨域的HTTP方法,多个方法之间用逗号隔开
  • Access-Control-Allow-Headers:仅在预检请求的响应中指定有效,用于表明服务器允许携带的首部字段。多个首部字段之间用逗号隔开
  • Access-Control-Max-Age:指明本次预检请求的有效期,单位为秒。在有效期内,预检请求不需要再次发起
  • Access-Control-Allow-Credentials:为true时,浏览器会在接下来的真实请求中携带用户凭证信息(cookie等),服务器也可以使用Set-Cookie向用户浏览器写入新的cookie

简单请求:

  • 定义:不携带自定义请求头信息的GET请求、HEAD请求,以及 Content-Type为application/x-www-form-urlencoded、multipart/form-data或text/plain的POST请求
  • 浏览器在发起请求时,会在请求头中自动添加一个 Origin 属性,值为当前页面的 URL 首部。当服务器返回响应时,若存在跨域访问控制属性,则浏览器会通过这些属性判断本次请求是否被允许
  • 只需后端在返回的响应头中添加Access-Control-Allow-Origin字段并填入允许跨域访问的站点即可

预检请求:

  • 会发送一个 OPTIONS 请求到目标站点,以查明该请求是否安全,防止请求对目标站点的数据造成破坏
  • 定义:以 GET、HEAD、POST 以外的方法发起;或者使用POST方法,但请求数据为application/x-www-form-urlencoded、multipart/form-data和text/plain以外的数据类型;再或者,使用了自定义请求头,则都会被当成预检请求类型处理

带凭证的请求:

  • 定义:携带了用户cookie等信息的请求
  • jquery ajax设置方法:xhrFields : {withCredentials : true}
  • 浏览器在实际发出请求时,将同时向服务器发送 cookie,并期待在服务器返回的响应信息中指明 Access-Control-AllowCredentials为true,否则浏览器会拦截,并抛出错误

2、Spring Boot跨域解决方案

下面介绍Spring Boot三种跨域解决方案:

  • (配置单个Controller)@CrossOrigin注解:dispatcherServlet中处理,原理是CorsInterceptor
  • (全局配置)重写WebMvcConfigurer.addCorsMappings方法:dispatcherServlet中处理,原理是CorsInterceptor
  • (推荐)CorsFilter:由filter处理,要早于前两种

Spring MVC的请求顺序一般是:浏览器->filter->DispatcherServlet->interceptor->controller,因此用CorsFilter会更早地去处理跨域,效果也更好。

配置CorsFilter如下代码所示。

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 CrossoriginConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter(){
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfig = new CorsConfiguration();
// cors配置
// 后端服务器(http://localhost:8182)为资源提供方,AllowedOrigins可配置哪些网站可以获取我的资源
corsConfig.setAllowedOrigins(Arrays.asList("http://localhost"));
//corsConfig.setAllowedOrigins(Arrays.asList("*"));
// 预检请求的响应中有效
corsConfig.setAllowedMethods(Arrays.asList("GET","POST"));
corsConfig.setAllowedHeaders(Arrays.asList("Content-Type"));
//corsConfig.setMaxAge(Duration.ofHours(1L));
corsConfig.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",corsConfig);
registrationBean.setFilter(new CorsFilter(source));
registrationBean.setOrder(-1);
return registrationBean;
}
}

1、服务端代码

import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/getName")
public String getName(){
return "用户的名称为张三";
}

@PostMapping("/save")
public String save(@RequestBody Map<String,String> user, HttpServletRequest request, HttpServletResponse response){
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
System.out.println("username:"+user.get("username"));
System.out.println("password:"+user.get("password"));

// 测试跨域携带cookie
boolean hasMyCookie = false;
if(null != request.getCookies() && request.getCookies().length > 0){
for (Cookie cookie : request.getCookies()) {
if("sb-co".equals(cookie.getName())){
hasMyCookie = true;
}
System.out.println(cookie.getName()+":"+cookie.getValue());
}
}
if(!hasMyCookie){
Cookie cookie = new Cookie("sb-co", format.format(new Date()));
cookie.setMaxAge(3600);
response.addCookie(cookie);
}
return "保存用户成功";
}
}

2、前端页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery-3.4.1.js"></script>
<script>
$(function () {
$("#getName").click(function () {
$.ajax({
url: "http://localhost:8182/user/getName",
method : "get",
success:function (result) {
alert(result)
}
});
});
$("#saveUser").click(function () {
$.ajax({
url: "http://localhost:8182/user/save",
method : "post",
contentType : "application/json;charset=UTF-8",
data : JSON.stringify({
"username":"孙悟空"
}),
xhrFields : {withCredentials : true},
success:function (result) {
alert(result)
}
});
});
});
</script>
</head>
<body>
<button id="getName">简单请求-获取用户名称</button>
<button id="saveUser">预检请求:保存用户</button>
</body>
</html>

3、运行服务

将静态资源文件放到某个目录下,然后在nginx.conf中配置location,如下所示。

# server模块下;root为静态资源存放目录
server {
listen 80;
location / {
root html/SpringMvc;
index index.html index.htm;
}
}

然后启动nginx,作为静态资源服务器。

启动Spring Boot。

浏览器访问​​http://localhost​​ ,如下图所示。

Spring Boot三种跨域解决方案与Spring Security跨域解决方案_spring


分别测试简单请求和预检请求,观察浏览器控制台Network,如下图所示。

Spring Boot三种跨域解决方案与Spring Security跨域解决方案_cors_02


preflight就是预检请求。状态都是200,说明跨域是OK的。通过UserController.save方法的代码可知,第一次请求时会response会携带cookie “sb-co”,这是一个跨域cookie。当我们再次请求时,由于我们配置了允许跨域cookie,请求里会携带“sb-co”,后端控制台也能打印该cookie的值,如下图所示。

Spring Boot三种跨域解决方案与Spring Security跨域解决方案_spring_03

3、Spring Security跨域解决方案

引入spring security框架后,第1、2种方案都会失效,第3种方案如果过滤器的优先级要低于spring security的优先级也会失效。

如果配置的是第1、2种方案或低优先级filter的第3种方案,预检请求会先到达spring security的过滤器,由于预检请求不会携带任何凭证信息,因此会被拦截下来。

spring security解决方案:

  • 使用第1、2种方案或低优先级filter的第3种方案:需要让spring security对options类型的预检请求(不会携带凭证)放行
  • 使用高优先级的filter:优先级必须高于spring security过滤器链中最高优先级的过滤器
  • 专业解决方案:开启cors

以下有三种方式可以开启spring security的cors:

  • 手动指定一个CorsConfigurationSource实例
  • 声明一个CorsFilter的Spring Bean
  • 将CorsConfigurationSource实例声明为Spring Bean,不用手动指定