Fork me on GitHub

细说ajax跨域

文章概述

本篇文章细说ajax跨域。

跨域

跨域指的是web网站访问与自己域名不同的其它地址的请求;

实质

跨域实质是浏览器安全验证限制了跨域请求;

1
当浏览器发出跨域请求时,浏览器会在请求头中添加相关的跨域信息,经过服务器处理后,响应头里如果没有对应的跨域信息,将会发生跨域失败;

浏览器处理请求

两种情况:简单请求和非简单请求;

浏览器在发送跨域请求时,对简单请求会直接执行,对非简单请求会先发出一个OPTION预检命令,校验通过后再执行请求;

简单请求
  • 请求方法为:get/head/post
  • 请求头里:无自定义头,Content-Type为:text/plain、multipart/form-data、application/x-www-form-urlencoded
非简单请求
  • put/delete方法的ajax请求;
  • 发送json格式的ajax请求;
  • 带自定义请求头的ajax请求;

http请求处理流程

请求从浏览器发出,请求到达http服务器(主要apache或nginx来处理),静态请求(如图片等)会被http服务器处理后直接返回,动态请求则会被转发到后台的应用服务(tomcat)来处理,处理完结果会发给http服务器,http服务器再转给浏览器。

产生跨域的条件

以下三个条件同时发生时,才会发生跨域问题:

1> 浏览器限制:

浏览器出于安全考虑,当发现你的请求跨域的时候,会主动做安全校验,如果校验不通过就会报跨域安全问题,如果后台没有任何限制,此时后台是接收到了请求并正确返回的,只是浏览器自身报错;

2> 跨域;

服务器后台不允许前台调用;

3> XHR(XMLHttpRequest)请求

浏览器发出的请求如果不是XHR请求,则不会进行安全验证,ajax默认发出的请求就是XHR请求;

前端跨域示例

定义不同域的基本地址base,有以下几种跨域请求示例;

普通跨域请求

1
2
3
 $.getJSON(base + "/get").then(function (jsonObj) {
result = jsonObj;
});

jsonp

1
2
3
4
5
6
7
8
9
$.ajax({
url: base + "/get",
dataType: "jsonp",
jsonp: "callback",
cache: false,
success: function (json) {
result = json;
}
});

非简单请求跨域

json格式请求,预检命令与缓存预检命令

1
2
3
4
5
6
7
8
9
10
$.ajax({
type: "post",
url: base + "/postJson",
// 发送json类型的请求
contentType: "application/json;charset=utf-8",
data: JSON.stringify({name: "jason"}),
success: function (json) {
result = json;
}
});

带cookie跨域

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
type: "get",
url: base + "/getCookie",
//发送ajax请求会带cookie
xhrFields: {
withCredentials:true
},
success: function (json) {
result = json;
}
});

自定义请求头跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.ajax({
type: "get",
url: base + "/getHeader",
//添加请求头方式1:
headers: {
"x-header1": "AAA"
},
//添加请求头方式2:
beforeSend: function (xhr) {
xhr.setRequestHeader("x-header2", "BBB")
},
success: function (json) {
result = json;
}
});

解决跨域的思路

  • 客户端改动:让浏览器不做安全验证;
  • 控制发出去的请求非XHR类型,JSONP方式通过动态创建script,在script中发出跨域请求,弊端是只能发Get请求;
  • 实现跨域:
1
2
1> 被调方(一般是服务端)做修改,支持跨域,被调用方通知调用方的浏览器跨域允许调用方跨域访问;
2> 调用方(一般是客户端)做修改,隐藏跨域,通过代理让浏览器发出去请求,都是网站本域域名,再在代理里面把发出的指定的url请求转到服务端域名里,浏览认为是在同一个域名里;

禁用浏览器限制解决跨域

windows环境,cmd命令行进入打开chrome.exe文件路径,输入以下命令启动一个没有安全验证的chrome浏览器,即可跨域请求。

1
2
$ chrome --disable-web-security --user-data-dir=f:\temp
// 注:f:\temp为临时路径,存放chrome临时文件;

Jsonp跨域

发送非xhr类型的请求,浏览器不会做安全校验;

概述

  • Jsonp(json with padding) ,是json的补充使用方式,利用script标签请求资源可以跨域来解决跨域问题的。jsonp通过动态创建script,在script里面把请求发出的;
  • 普通的ajax请求请求和返回的类型Content-Type是json,jsonp请求方式,是javascript;
  • 改动范围:前后台都需要改动;

    注意:Jsonp请求返回的数据结构是:callback的参数值作为函数名,返回的数据作为参数;

请求步骤

  1. 前端:jsonp的请求方式,默认自动给请求加了callback参数,后台发现有callback就认为是jsonp请求。
  2. 后台:ajax发送jsonp请求可以指定参数名,对应后台也需要修改,来识别对应的参数名;

前端发Jsonp请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义请求
$.ajax({
url: base + "/getCrossDomain",
dataType: "jsonp", // 返回的参数类型
jsonp: "callback", // jsonp参数名
cache: false, //请求是否缓存
success: function (json) {
result = json;
}
});
//----------------------------------------------
【浏览器发请求细节】
// 请求参数
// _: 1527406132040 表示请求不允许被缓存,如果允许则没有此值;
callback: jQuery33102942401312227503_1527406132039
_: 1527406132040
// 请求返回结果
jQuery33102942401312227503_1527406132039({"data":"getCrossDomain"});

后端改动

spring-boot

java后台,spring-boot框架;

添加Jsonp支持类

在项目中添加Jsonp支持类:JsonpAdvice

1
2
3
4
5
6
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}
}

缺点

  • 需要服务端修改代码;
  • 只支持get请求;
  • 发送的不是XHR请求;

支持跨域

支持跨域的两种方式:

  1. 被调用方支持跨域(前端访问后端的地址是绝对地址);
  2. 调用方隐藏跨域(前端访问后端的地址是相对地址);

被调用方服务支持跨域

前端访问后端的地址是绝对地址

请求处理流程

调用方请求从浏览器发出,直接请求被调用方http服务器,被调用方的http服务器根据动态请求则会交给应用服务器(tomcat)来处理,处理完成后,结果传给http服务器,最后发给浏览器。

跨域思路

被调用方服务器根据http请求协议,在响应头中返回一些标志字段,来告诉浏览器允许调用方跨域请求。

JavaEE架构里配置跨域响应头:

1
2
3
4
1. 应用服务器过滤请求,添加跨域响应头;
2. http Apache服务配置;
3. http Nginx服务配置;
4. Tomcat应用服务器配置;

Filter跨域

概述
  • Filter方案其实是调用放的浏览器直接发送请求到被调用方的应用服务器,应用服务器通过filter增加响应头的方式处理请求直接返回给调用方浏览器;
  • Filter方案过滤请求头信息,响应给浏览器允许跨域的信息;
后端过滤方案
springboot

通过过滤所有请求,来添加响应头,来实现全局接口跨域;

1> 在application程序入口中添加url请求拦截入口方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class AjaxServerApplication {
//...
@Bean
public FilterRegistrationBean<CrossFilter> registerBean(){
FilterRegistrationBean<CrossFilter> bean=new FilterRegistrationBean<CrossFilter>();
// 设置需要过滤的url地址:所有url
bean.addUrlPatterns("/*");
// 设置Filter过滤对象
bean.setFilter(new CrossFilter());
return bean;
}
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 跨域url过滤器
*/
public class CrossFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 处理过滤到的url请求的逻辑
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 设置允许跨域调用的地址域
// response.addHeader("Access-Control-Allow-Origin","http://localhost:8081");
// response.addHeader("Access-Control-Allow-Methods","GET");

//--Access-Control-Allow-Origin-----------------------------------

// 设置允许所有地址和方法跨域,注意请求带cookie时,origin必须全匹配,不能使用通配符来匹配所有请求域,应该动态获取origin;
// response.addHeader("Access-Control-Allow-Origin","*");

HttpServletRequest request = (HttpServletRequest) servletRequest;
String origin = request.getHeader("Origin");
if (!StringUtils.isEmpty(origin)) {
response.addHeader("Access-Control-Allow-Origin", origin);
}

// 非简单请求:
// 发送json格式的请求,会发出预检命令,检查响应头是否有Content-Type;
// response.addHeader("Access-Control-Allow-Headers","Content-Type");
// 支持多个自定义请求头(不建议写死,应该动态添加请求头)
// response.addHeader("Access-Control-Allow-Headers","Content-Type,x-header1,x-header2");
// 动态支持所有跨域请求头
// 动态获取所有跨域请求头
String headers = request.getHeader("Access-Control-Request-Headers");
if (!StringUtils.isEmpty(headers)) {
response.addHeader("Access-Control-Allow-Headers", headers);
}

// 允许所有方法
response.addHeader("Access-Control-Allow-Methods", "*");

// 浏览器在一次预检命令校验成功后,会缓存预检命令的结果,3600s内不会重新发出预检命令;
response.addHeader("Access-Control-Max-Age", "3600");

// 支持cookie
response.addHeader("Access-Control-Allow-Credentials", "true");

// 重新设置调用链
filterChain.doFilter(servletRequest, response);
}

@Override
public void destroy() {

}
}

nginx跨域

此处指后端对应的nginx http服务器。

虚拟主机

虚拟主机是:多个域名指向同一个服务器,服务器根据不同的域名吧请求转到不同的应用服务器,看上去有很多个主机,实际上只有一个主机。

配置跨域虚拟主机

按以下步骤配置支持跨域的虚拟主机,即可完成简单的nginx服务支持跨域;

1> 在host文件中添加本地域名映射
(c:\Windows\System32\Drivers\etc\host),主要目的是通过xem.com域名在本地模拟访问后端服务器;

1
2
3
# 即访问xem.com域名时,映射到本机地址;
# 127.0.0.1(本机地址)
127.0.0.1 xem.com

2> nginx\conf目录下创建vhost文件,用于存放每个虚拟主机域名的conf文件,并修改nginx配置文件nginx/conf/nginx.conf,添加以下代码,表示加载vhost文件夹里的虚拟主机配置:

1
2
3
4
http{
//...
include /vhost/*.conf;
}

3> 配置虚拟主机:vhost文件夹中,新建一个xem.com.conf的域名虚拟主机配置文件,用于监听该域名的请求,将请求转到相应的应用服务器;

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
server{
# 监听80端口
listen 80;
# 当用户访问xem.com时,此虚机主机进行处理
server_name xem.com;

# /匹配所有请求
location /{
# 80端口的xem.com域名的所有请求都转到localhost:8080
proxy_pass http://localhost:8080/;

# 支持跨域的固定响应头
add_header Access-Control-Allow-Methods *;
add_header Access-Control-Max-Age 3600;
add_header Access-Control-Allow-Credentials true;

# 支持跨域的动态响应头(nginx属性值都要小写)
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Headers $http_access_control_request_headers;

# 处理跨域的预检命令,直接返回200(注意:if后面有空格)
if ($request_method = OPTIONS){
return 200;
}
}
}

4> 启动nginx校验跨域;

nginx根目录下,执行如下相关nginx命令:

  • 校验配置文件书写是否正确
1
$ nginx.exe -t
  • 启动nginx
1
$ start nginx.exe
  • 重新加载nginx改动
1
$ nginx.exe -s reload
  • 停止nginx服务
1
$ nginx.exe -s stop

apache跨域

apache跨域和nginx原理一样,也是配置虚拟主机来设置响应头,最终支持跨域的;

配置步骤
  1. 本地host文件,添加xem.com域名映射,访问此域名时模拟访问后端服务器;
  2. 配置apache配置文件:打开Apache24\conf\httpd.conf配置文件,开启相关配置功能;

打开以下注释:

1
2
3
4
5
6
7
8
9
10
11
12
# 虚拟主机模块
LoadModule vhost_alias_module modules/mod_vhost_alias.so
# 虚拟主机配置文件
Include conf/extra/httpd-vhosts.conf

# proxy代理:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

# headers
LoadModule headers_module modules/mod_headers.so
LoadModule rewrite_module modules/mod_rewrite.so
  1. 配置虚拟主机:打开虚拟主机配置文件Apache24\conf\extra\httpd-vhosts.conf;

添加一个虚拟主机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<VirtualHost *:80>
ServerName xem.com
ErrorLog "logs/xem.com-error.log"
CustomLog "logs/xem.com-access.log" common
ProxyPass / http://localhost:8080/

# 动态请求头Origin的值返回到Access-Control-Allow-Origin字段
Header always set Access-Control-Allow-Origin "expr=%{req:origin}"

# 动态请求头Access-Control-Request-Headers值返回到Access-Control-Allow-Headers字段
Header always set Access-Control-Allow-Headers "expr=%{req:Access-Control-Request-Headers}"

# 固定值的请求头
Header always set Access-Control-Allow-Methods "*"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "3600"

# 处理预检命令,直接返回204
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ "/" [R=204,L]
</VirtualHost>
  1. 启动apache服务,验证跨域;

方式一: 命令方式启动;

1
2
3
4
// 启动服务
$ httpd.exe -k start
// 停止服务
$ httpd.exe -k stop

方式二:直接打开Apache24\bin\httpd.exe执行文件;

JavaEE后端跨域

spring-boot框架
过滤请求跨域

通过过滤所有url请求的方式,实现全局接口支持跨域;

详见Filter跨域;

全局接口跨域注解
  1. spring-boot 1.0的实现方式:
1
2
3
4
5
6
7
8
9
10
11
12
// 新建配置类,配置跨域信息;
@Configuration
public class CrossDomainSb1Config extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("*")
.maxAge(3600);
}
}
  1. spring-boot 2.0的实现方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 新建配置类,配置跨域信息;
@Configuration
@EnableWebMvc
public class CrossDomainSb2Config implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOrigins("*")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}
局部接口跨域注解

通过@CrossOrigin注解让部分接口Controller类或接口方法支持跨域;

  • 只需要在接口controller上或某个接口方法上加@CrossOrigin注解即可;
  • 配置@CrossOrigin并不支持cookie的请求,如需支持cookie,需要配置额外信息:@CrossOrigin(allowCredentials=”true”);
1
2
3
4
5
6
@RestController
@RequestMapping("/crossDomain")
@CrossOrigin(allowCredentials = "true")
public class AjaxApiController {
//...
}

调用方服务隐藏跨域

隐藏跨域是在调用方的http服务器进行配置,前端访问后端的地址是相对地址;

  • 隐藏跨域指调用方的请求从调用方的http服务器直接发送到被调用了方的http服务器;
  • 跨域请求是通过调用方http服务器的反向代理,将请求转发到被调用方的http服务器的,所以在浏览器上面看不到任何跨域请求的信息;

反向代理:访问同一个域名的两个不同的url,最后会去到两个不同的服务器;

nginx

前端http服务器是nginx服务器,配置nginx隐藏跨域;

1> host文件中,添加xem.com作为本地域名映射;

2> nginx/conf/vhost文件中添加域名虚拟主机配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server{
# 监听80端口
listen 80;
# 当用户访问xem.com时,此虚机主机进行处理
server_name xem.com;

# /xem.com访问的都是localhost:8081网页的地址
location /{
proxy_pass http://localhost:8081/;
}

# 网页内相对路径代理后端地址
location /ajaxserver{
proxy_pass http://localhost:8080/;
}
}

3> ajax请求使用相对地址“/ajaxserver”拼接具体请求;

4> 启动nginx服务,访问xem.com网页,请求会自动转发到后台的地址;

apache

1> 设置host文件中的域名映射;

2> 同被调用方支持跨域里的apache的配置类似,不同的是虚拟主机的配置,如下:

1
2
3
4
5
6
7
8
9
10
<VirtualHost *:80>
ServerName xem.com
ErrorLog "logs/xem.com-error.log"
CustomLog "logs/xem.com-access.log" common

# ajaxserver相对路径代理后台服务端地址
ProxyPass /ajaxserver http://localhost:8080/
# xem.com域名代理访问的网站真是地址
ProxyPass / http://localhost:8081/
</VirtualHost>

3> 网页使用相对地址请求服务端接口;

4> 启动apache服务测试跨域;

第三方js库解决跨域

在编写angular/reactjs/vue项目时,有他们自己的跨域解决方案;

坚持原创技术分享,您的支持将鼓励我继续创作!