前言
在前后端分离的项目中,前端一般是通过Ajax请求接口,如果前端和后台不在同一个服务器和端口,那么就会出现跨域的问题,比如当localhost:8081网站访问localhost:8080网站资源就会存在跨域问题。
同源策略
很多人对跨域有一种误解,以为这是前端的事,和后端没关系,其实不是这样的,说到跨域,就不得不说说浏览器的同源策略。
同源策略是由Netscape提出的一个著名的安全策略,它是浏览器最核心也最基本的安全功能,现在所有支持JavaScript的浏览器都会使用这个策略。所谓同源是指协议、域名以及端口要相同。同源策略是基于安全方面的考虑提出来的,这个策略本身没问题,但是我们在实际开发中,由于各种原因又经常有跨域的需求,传统的跨域方案是JSONP,JSONP虽然能解决跨域但是有一个很大的局限性,那就是只支持GET请求,不支持其他类型的请求,而今天我们说的CORS(跨域源资源共享)(CORS,Cross-origin resource sharing)是一个W3C标准,它是一份浏览器技术的规范,提供了Web服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,这是JSONP模式的现代版。
Spring从4.2版本开始对于CORS也提供了相应的解决方案,今天我们就来看看SpringBoot中如何实现CORS。
模拟跨域
首先创建两个普通的SpringBoot项目,第一个命名cors1提供服务,默认配置端口为8080,第二个命名为cors2消费服务,配置配置为8081,然后在cros1上提供两个hello接口,一个get,一个post,如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String helloGet(){
return "hello get";
}
@PostMapping("/hello")
public String helloPost(){
return "hello post";
}
}
在cors2r的resources/static目录下创建一个test.html文件,发送一个简单的ajax请求,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery-3.3.1.js"></script>
</head>
<body>
<dev id="res"></dev><br/>
<button onclick="funget()">get</button>
<button onclick="funpost()">post</button>
<script>
function funget() {
$.ajax({
url:"http://localhost:8080/hello",
type:"get",
success:function (msg) {
$("#res").html(msg);
}
})
}
function funpost() {
$.ajax({
url:"http://localhost:8080/hello",
type:"post",
success:function (msg) {
$("#res").html(msg);
}
})
}
</script>
</body>
</html>
然后分别启动两个项目,访问localhost:8081/test.html页面发送请求按钮,观察浏览器控制台如下:
可以看到,由于同源策略的限制,请求无法发送成功。
使用CORS可以在前端代码不做任何修改的情况下,实现跨域,那么接下来看看在cors1中如何配置。首先可以通过@CrossOrigin注解配置某一个方法接受某一个域的请求,如下:
@RestController
public class HelloController {
@CrossOrigin(value = "http://localhost:8081")
@GetMapping("/hello")
public String helloGet(){
return "hello get";
}
@CrossOrigin(value = "http://localhost:8081")
@PostMapping("/hello")
public String helloPost(){
return "hello post";
}
}
这个注解表示这两个接口接受来自http://localhost:8081地址的请求,配置完成后,重启cors1,再次发送请求,浏览器控制台就不会报错了,cors2也能拿到数据了。
此时观察浏览器请求网络控制台,可以看到响应头中多了如下信息:
这个表示服务端愿意接收来自http://localhost:8081的请求,拿到这个信息后,浏览器就不会再去限制本次请求的跨域了。
cros1上,每一个方法上都去加注解未免太麻烦了,我们还可以将@CrossOrigin注解加在一个类上:
@RestController
@CrossOrigin(value = "*") //*表示可以接受任意网站发来的请求
public class HelloController {
@GetMapping("/hello")
public String helloGet(){
return "hello get";
}
@PostMapping("/hello")
public String helloPost(){
return "hello post";
}
}
在Spring Boot中,还可以通过全局配置一次性解决这个问题,全局配置只需要cors1中添加一个配置类,在配置类中重写addCorsMappings方法即可,如下:
@Configuration
public class MvcWebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许跨域的接口,/**表示所有
.allowedOrigins("http://localhost:8081") //允许访问的客户端域名
.allowedHeaders("*") //允许通过的请求头,*表示所有
.allowedMethods("*") //允许访问的方法,默认为get、post、head,*表示所有
.maxAge(30*1000); //探测请求的有效期,除了get请求不用探测
}
}
经过这样的配置之后,就不必在每个方法上单独配置跨域了。重新访问,依然是OK的!
存在的问题
了解了整个CORS的工作过程之后,我们通过Ajax发送跨域请求,虽然用户体验提高了,但是也有潜在的威胁存在,常见的就是CSRF(Cross-site request forgery)跨站请求伪造。跨站请求伪造也被称为one-click attack 或者 session riding,通常缩写为CSRF或者XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法,举个例子:
假如一家银行用以运行转账操作的URL地址如下:http://icbc.com/aa?bb=cc,那么,一个恶意攻击者可以在另一个网站上放置如下代码:< img src=“http://icbc.com/aa?bb=cc”>,如果用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会遭受损失。
基于此,浏览器在实际操作中,会对请求进行分类,分为简单请求,预先请求,带凭证的请求等,预先请求会首先发送一个options探测请求,和浏览器进行协商是否接受请求。默认情况下跨域请求是不需要凭证的,但是服务端可以配置要求客户端提供凭证,这样就可以有效避免csrf攻击。