跨域访问,是指从一个域名的网页去请求另一个域名的资源。比如从www.baidu.com 页面去请求 www.google.com 的资源。但是一般情况下不能这么做跨域访问,因为有浏览器的“同源策略”存在,这是浏览器对JavaScript施加的安全限制。
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
“同源”是指三个相同,协议相同、域名相同、端口相同,只有有任何一个地方不同,就认为是跨域。
随着互联网的发展,“同源政策”越来越严格,目前,如果非同源(跨域),共有三种行为受到限制:
Cookie 、LocalStorage、IndexDB 无法访问
DOM 无法获取
AJAX请求不能发送
我们在项目中,需要设置对跨域访问的支持,是因为项目的架构需要,例如
公司内部有多个不同的子系统,例如A和B,分别部署在不同的服务器上,其域名也不相同
由于公司内部的数据需要,现在A系统中,跨域访问B系统,从而获取内部的一些信息资源
在前后端分离的项目中,前端页面部署在一个服务器上,后端项目部署在另一个服务器上,从前端页面上发送ajax请求到后端系统中,这种情况,就属于跨域访问
新建项目,springboot-html
页面代码:ajax.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<!--<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>-->
<!--不需要写static-->
<script src="js/jquery.js"></script>
<script>
$(function(){
$(".btn1").on("click",function(){
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
});
});
</script>
</head>
<body>
<div>
<button class="btn1">test1</button>
</div>
</body>
</html>
pom文件
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
配置文件application.properties
server.port=9999
Controller
package com.briup.cms.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/ajax")
public String hello() {
return "ajax";
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
配置文件,application.properties
server.port=8989
Controller
package com.briup.cms.web.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hi")
public String hello() {
return "hello world";
}
}
启动,访问
访问springboot-html项目中的ajax.html页面,点击页面中的按钮,发送ajax请求,访问springbootcors
项目中的接口:
当前页面的访问地址: http://localhost:9999/ajax
点击按钮后请求地址: http://localhost:8989/hi
可以看出,此时这俩个地址,属于“非同源”,本次访问属于跨域访问。
点击后,控制台上输出的错误信息:
已拦截跨源请求:同源策略禁止读取位于 http://localhost:8989/hi 的远程资源。
(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
点开网络模块,查看具体的请求信息:
请求头中,自动添加了信息 Origin: http://localhost:9999 ,通知服务器本次是跨域访问
从本次响应的内容中,可以看到,其实响应的内容已经成功返回了,但是由于浏览器的同源政策,把这些结果都给舍弃了。并且执行了Ajax中的回调函数error
避开同源政策给跨域访问带来的限制,方式有很多:
nginx
CORS
JSONP
WebSocket
Node.js
document.domain+iframe
other
Nginx是一款开源的、高性能的HTTP和反向代理服务器
也是一个IMAP/POP3/SMTP代理服务器
由C语言编写,其系统开销、CPU使用效率都很优秀
性能稳定、功能丰富、配置简洁
服务器中没有web容器
正向代理,一般需要自己手动进行配置:(已知代理服务器)
代理服务器和客户端在同一个网络中。
反向代理,一般是指以代理服务器来接收网络上的请求,然后将请求再转发给内部网络上的其他服务器:
代理服务器和Web服务器在同一个网络中。一般不需要用户自己设置服务器,用户甚至感受不到自己访问的是反向代理服务器。
例如,Nginx的反向代理功能,解决访问跨域问题,或者实现多个服务器负载均衡的效果
特别注意,nginx启动的时候,一定不要存放在有中文的路径中,如果启动失败,查看错误的日志记录
修改nginx的配置文件nginx.conf
conf.config
location /html {
rewrite ^/html/(.*)$ /$1 break;
proxy_pass http://localhost:9999/;
}
当访问指定地址 http://localhost/html 的时候,会先重写URI地址
URI符合正则表达式^/api/(.*)$ 的地址会被重写为 /$1
$1表示正则表达式中第一个()中的字符串
最后把重写后的新地址,转发给http://localhost:9999/
location /api {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://localhost:8989/;
}
ajax页面中,新增按钮和ajax请求
$(".btn2").on("click",function(){
$.ajax({
type: "GET",
url : "/api/hi",
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
});
<button class="btn2">test2button>
启动nginx服务器,访问地址
使用nginx之后,访问springboot-html中页面的地址: http://localhost/html/ajax
但其实是使用nginx代理服务器来“欺骗”浏览器,让浏览器认为是 “同源访问”。浏览器中,从始至终都是访问http://localhost/ 下面的资源,所以浏览器认为一直是“同源访问”。浏览器不知道的是,它访问的服务器是一个nginx代理服务器,它会接收浏览器请求,重写URI地址后,转发给真正的服务器。
1、概念
CORS(Cross-origin resource sharing),是一个W3C标准,全称是”跨域资源共享”。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息。(Origin)
有时还会多出一次附加的请求,但用户不会有感觉。(options方式的请求)
2、分类
浏览器将CORS请求分成两类:
简单请求(simple request)
非简单请求(not-so-simple request)
只要同时满足以下两大条件,就属于简单请求:
请求方法是以下三种方法之一
GET
POST
HEAD
HTTP的头信息不超出以下几种字段
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type,该字段的值只能是以下三种
application/x-www-form-urlencoded
multipart/form-data
text/plain
注意,只要不能同时满足上面两个条件,就属于非简单请求。
对于简单请求,浏览器直接发出CORS请求,同时在请求头中增加一个Origin字段。
该字段表示,本次请求来自哪个源(协议 + 域名 + 端口),服务器根据这个值,决定是否同意这次请
求。
Origin指定的源,即使不在服务器许可范围内,服务器还是会返回一个正常的HTTP响应,但是响应头中
不含指定Access-Control-Allow-Origin字段,浏览器这时候就知道本次跨域访问失败。
但是这个时候,响应状态可能是200,同时在一些工具中还能看到正确的返回值。
例如,
其中,
浏览器在请求头中自动添加了Origin 字段
服务器在响应中没有添加Access-Control-Allow-Origin 字段(说明服务器不支持此请求跨域访
问)
响应的状态码是200,并且从响应内容中可以看到正确的返回内容
同时,浏览器会抛出一个错误,被ajax的核心对象XMLHttpRequest的onerror 回调函数捕获
如果Origin指定的域名在服务器的许可范围内,服务器返回的响应,会多出几个头信息字段:
Access-Control-Allow-Origin
该字段是必须的。
它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的跨域请求。
Access-Control-Allow-Credentials
该字段可选
表示是否允许发送Cookie。
设为true,即表示服务器已经允许了,跨域请求中可以携带Cookie。
但是这需要AJAX中设置withCredentials=true进行配合。
如果服务器不要浏览器发送Cookie,删除该字段即可。
Access-Control-Expose-Headers
该字段可选
列出了哪些首部可以作为响应的一部分暴露给外部。
默认情况下,只有七种可以暴露给外部
Cache-Control
Content-Language
Content-Length
Content-Type
Expires
Last-Modified
Pragma
如果想要让客户端可以访问到其他的首部信息,可以将它们在Access-Control-Expose-Headers里面指定
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段
的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词
和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
例如:
$(".btn1").on("click",function(){
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
contentType: "application/json",
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
});
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials (标签允许客户端携带验证信息,例如 cookie)
ajax跨域访问的时候,如果需要在请求中携带cookie,需要有以下设置:
1.前端ajax请求中,指定withCredentials属性为ture
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
xhrFields:{
withCredentials: true
},
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
2.后端响应头中,设置Access-Control-Allow-Credentials属性为true
//允许跨域携带cookie
response.setHeader("Access-Control-Allow-Credentials", "true");
3.后端响应头中,设置Access-Control-Allow-Origin属性为具体的值,而不是能通配符*
//response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:9999");
@RestController
public class HelloController {
// 允许cookie的情况下 前端配置 后台配置
@GetMapping("/hi")
public String hello(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", "http://localhost:9999");
return "hello world";
}
// 不允许cookie的情况下
@GetMapping("/hi")
public String hello(HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", "*");
return "hello world";
}
}
可添加拦截器(了解)
package com.briup.cms.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class CosInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Origin", "http://localhost:9999");
response.setHeader("Access-Control-Allow-Methods","POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, X-Custom-Header, Authorization");
System.out.println("跨域");
return true;
}
}
package com.briup.cms.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 com.briup.cms.web.interceptor.CosInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(cosInterceptor())
.addPathPatterns("/**");
}
@Bean
public CosInterceptor cosInterceptor() {
return new CosInterceptor();
}
}
注意,这里后端代码使用的使用response设置的,在springmvc中,也可以专门的设置方法来代替这种方式
springboot-html项目中的ajax.html页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<!--<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>-->
<script src="js/jquery.js"></script>
<script>
$(function(){
$(".btn1").on("click",function(){
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
});
$(".btn2").on("click",function(){
$.ajax({
type: "GET",
url : "/api/hi",
success: function(msg){
console.log(msg);
},
error: function(){
alert('error');
}
});
});
$(".btn3").on("click",function(){
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
success: function(msg){
console.log(msg);
},
error: function(msg){
alert('error');
}
});
});
});
</script>
</head>
<body>
<div>
<button class="btn1">test1</button>
<button class="btn2">test2</button>
<button class="btn3">test3</button>
</div>
</body>
</html>
springboot-cors项目中的Controller:
package com.briup.cms.web.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hi")
public String hello() {
return "hello world";
}
}
springboot-cors项目中的配置类:
package com.briup.cms.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 WebConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")//映射所有路径
.allowedOrigins("*")//运行所有客户端访问
.allowCredentials(false)//不允许携带cookie
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")//
支持的方法
.allowedHeaders("*")//运行所有请求头字段
.maxAge(3600);//允许客户端缓存“预检请求”中获取的信息,3600秒
}
}
启动俩个项目,访问页面,点击按钮,测试ajax跨域访问:
可以看出,此时ajax请求进行跨域访问,已经成功
注意,此时可以尝试在ajax请求中,设置contentType: application/json ,观察是否会发出“预检”请求
同时,也可以修改ajax请求,让其写的cookie和指定字段的头部:
$(".btn3").on("click",function(){
$.ajax({
type: "GET",
url : "http://localhost:8989/hi",
xhrFields:{
withCredentials: true
},
headers:{
token:"aaa.bbb.ccc"
},
contentType: "application/json",
success: function(msg){
console.log(msg);
},
error: function(msg){
alert('error');
}
});
});
此时,对应的后端跨域设置为:
@Configuration
public class WebConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:9999")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")//映射所有路径
.allowedOrigins("*")//运行所有客户端访问
.allowCredentials(false)//不允许携带cookie
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")//支持的方法
.allowedHeaders("*")//运行所有请求头字段
.maxAge(3600);//允许客户端缓存“预检请求”中获取的信息,3600秒
}
在此配置中,如果设置allowCredentials(true) ,那么allowedOrigins(“*”) 这里就不能使用通
配符了,必须要写一个或者多个(可变参数)客户端的地址
例如, allowedOrigins(“http://127.0.0.1:9999")
另外,如果只是想让Controller中的某一个方法或者几个方法被跨域访问,那么可以在方法上使用
@CrossOrigin 注解,例如
@RestController
public class HelloController {
@CrossOrigin(origins = {"http://127.0.0.1:9999"})
@GetMapping("/hi")
public String hello() {
return "hello world";
}
}
此时就不需要在配置类中做其他配置了,直接一个注解就可以让该方法被跨域访问了