前后端跨域解决方案总结

产生跨域的原因

  • 浏览器限制
  • 请求链接跨域
  • 请求类型是XMLHttpRequest

跨域解决方法之一:JSONP

​ 实现jsonp,需要前后端都需要改造,后端需要增加支持返回js函数,主要约定是url的key字段,由于jquery默认的jsonp请求的关键字key默认是callback,后端一般都会使用callback关键字,当然也可以定义另一个名称,而这个key对应的value使用什么都可以,jquery是使用随机数,后台会获取这个随机数,作为返回js函数的函数名。这是大多数情况,原理是如此,但可能有所不同。

​ 除此之外,还需要注意要使用随机数,不然jsonp由于是js脚本,如果请求参数相同,就会使用缓存的结果,而不会使用最新的结果,jquery的jsonp默认有随机数参数

​ jsonp有以下弊端:服务端也需要改造,请求只能是get。由于请求是get,一般只能用来请求一些获取数据的接口,而不能用于修改用户信息等类型的接口(需要用到post)。由于这个缺点,一般很少接口会用到jsonp,解决跨域问题还需要找其他解决办法

网络请求的路径和解决思路

​ 一般请求路径是:Client(调用方) - Apache/Nginx(http服务器) - Tomcat(被调用方后台应用服务器)。其中Apache/Nginx会处理来自client的请求,判断该请求为什么类型的请求,如果是静态请求,则直接处理返回给client,如果是动态请求,则转发给后台的应用服务器,处理完毕后则原路返回。

​ 动态请求指,跟用户数据有关的请求,xhr等

​ 静态请求指,跟用户数据无关的请求,例如img,js,css

​ 解决跨域的思路有两种,一种是在被调用方后台应用服务器上做处理,在响应头上增加字段,告诉浏览器允许对方调用,浏览器就不会报跨域问题;第二种是隐藏跨域的思路,在http服务器上做处理,做一个http请求转发,浏览器发现所有的请求都是同一个域,就不会报跨域问题。

被调用方解决 - 支持跨域(应用服务器配置响应头、Apache/Nginx服务器配置响应头CORS)

​ 当浏览器进行跨域请求时,会在预检请求的时候在请求头加上Origin字段,对应的值是当前域的信息。然后返回的时候会检查响应头里有没有允许跨域的字段,如果没有,浏览器就会报错拦截。具体检查的响应头字段是Access-Control-Allow-Origin,它是html5新增的一项标准,IE10以下是不支持的。如果需要IE10以下都完美支持跨域,请使用http服务器转发(http反向代理)解决问题。

​ 一般这个方法,后台应用服务器简称为filter,添加filter组件后,再在响应头里添加Access-Control-Allow-Origin(允许跨域的域名),Access-Control-Allow-Methods(允许跨域的请求类型)

1
2
// 后台设定返回
res.addHeader('Access-Control-Allow-Methods', '*');

简单请求和复杂请求(跨域时才区分)

​ 浏览器在进行跨域请求时,会判断该请求是简单请求还是非简单请求,如果是简单请求,则会先执行,后判断;如果是非简单请求,浏览器就会先发一个预检命令(OPTIONS),通过之后再发跨域请求。

工作中比较常见的【简单请求】:

  • 方法为GET、HEAD、POST

  • 请求header里无自定义请求

  • Content-Type为以下几种:

    • text/plain(表单数据以纯文本形式进行编码)
    • multipart/form-data(一般上传文件时会用到)
    • application/x-www-form-urlencoded(表单数据编码为键值对,&分隔)

工作中比较常见的【非简单请求】:

  • put、delete方法的ajax请求
  • 发送json格式的ajax请求(Content-Type为appliation/json;charset=utf-8)
  • 带自定义头的ajax请求

​ 发送json格式的ajax请求时,预检命令,请求头会加上一个Access-Control-Allow-Headers为Content-Type,询问后台应用服务器是否允许这个头。如果后台没有在filter里加上这个响应头告诉浏览器允许这个header,浏览器发现没有通过的信息就会报错

1
2
// 后台设定返回
res.addHeader('Access-Control-Allow-Headers', 'Content-Type');

​ 非简单请求一共会发送两次请求,第一次是OPTIONS预检命令,第二次才是真正的跨域请求

​ OPTIONS预检命令是可以做缓存的,由于非简单请求每次都会发送两次请求,这样会比较影响性能。后台应用服务器可以通过在响应头里增加Access-Control-Max-Age,值是毫秒数,这样浏览器在规定时间内,这个接口的请求就不会再进行OPTIONS预检命令,直接执行下一个请求。如果用户在浏览器开发模式中将Disable cache开启,那么就会清掉这个缓存,重新验证OPTIONS预检命令

1
2
// OPTIONS请求设置3600毫秒缓存
res.addHeader('Access-Control-Max-Age', '3600');

​ 注意,如果使用了反向代理解决跨域问题的时候,那么这些请求实际上都没有跨域,都在同一个域名上请求,不存在简单请求和复杂请求,就没有了OPTIONS这个预检请求了,直接进行请求

带cookie的跨域

​ ajax会自动带上同源的cookie,不会带上不同源的cookie。前端如果需要发起跨域的带有cookie的请求,需要将XMLHttpRequest.withCredentials置为true,修改方法为:

1
2
3
4
$.ajax({
// ...
xhrFields:{ withCredentials:true }
})

​ 这个修改永远不会影响到同源请求,不同域下的XmlHttpRequest 响应,不论其header 设置什么值,都无法为它自身站点设置cookie值,除非它在请求之前将withCredentials 设为true。

​ 以上是前端需要做的改造,后端需要做的改造如下:

1
2
res.addHeader('Access-Control-Allow-Origin', '具体网址');
res.addHeader('Access-Control-Allow-Credentials', 'true');

​ 注意,这时候后端应用服务器就不能将 Access-Control-Allow-Origin置为*这样的通配符形式,一定要用全匹配的地址,而且需要额外配置Access-Control-Allow-Credentials,通过这样的参数设置,就可以保持跨域ajax时传递cookies。

​ 这时注意,这也不是后台应用服务器将Access-Control-Allow-Origin写死,由前面得知,浏览器在跨域请求时,会在预检请求OPTIONS里增加一个Origin字段,这时候后台就可以取到这个字段,然后将它赋值给Access-Control-Allow-Origin,这也就变相得实现通配符的功能~

​ 需要非常注意,我们发送的cookie,是被调用方域名的cookie,cookie要加在被调用方的域名上,而不是调用方的cookie

1
document.cookie = "name=yuuhei; path=/";

带自定义头的跨域

​ jquery的ajax自定义请求有两种方法:

1
2
3
4
5
6
7
8
$.ajax({
// ...
headers: { token: 'ASDFGH' },
// 或
beforeSend: function(xhr) {
xhr.setRequestHeader('token', 'ASDFGH')
}
})

​ 后端也要做响应处理,不然浏览器会报Access-Control-Allow-Headers的问题,需要在后台代码加上对应名称的header(像加之前Content-Type的时候那样加),可以先获取由发起方得到的自定义header,再赋值这样的套路(像之前设定Origin时的方法),这样后台代码就不会写死。

express作为后台应用服务器解决方案总结(被调用方解决):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// nginx反向代理解决后这些都不用设置了
var allowCrossDomain = function(req, res, next) {
let origin = req.headers.origin;
// 允许域名
res.header('Access-Control-Allow-Origin', origin);
// 允许方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// 允许发json,客户端强制发送contentType为appliation/json;charset=utf-8,会有OPTIONS预检请求
res.header('Access-Control-Allow-Headers', 'Content-Type, Token');
// 允许跨域带cookie
res.header('Access-Control-Allow-Credentials','true');
// 预检命令缓存
res.header('Access-Control-Max-Age','3600');
next();
};

app.use(allowCrossDomain);

被调用方- Nginx解决方案

​ nginx有一个虚拟主机的概念,什么是虚拟主机:

多个域名指向同一个服务器,nginx服务器根据不同的域名,转发到不同的应用服务器

​ 虚拟主机的配置:配置host文件,设定如下类似代码,这样nginx就可以配置多个虚拟域名,其实访问的都是同一个ip地址:

1
2
3
# 127.0.0.1就是本地的ip,b.com就是映射的虚拟主机ip
# linux下使用sudo vim /etc/hosts修改保存
127.0.0.1 b.com

​ 配置nginx:打开nginx目录,打开conf目录,新建一个vhost文件夹,然后修改nginx.conf

1
2
# nginx.conf 在http块区域里最后面增加
include vhost/*.conf;

​ 在vhost文件夹创建一个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
# vhost文件夹新增.conf
server {
# 浏览器请求地址端口
listen 80;
# 浏览器请求地址
server_name b.com;

location / {
# 后台应用服务器地址
proxy_pass http://localhost:8080/;
# 在次修改响应头

add_header Access-Control-Allow-Methods *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Max-Age 3600;

add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Headers $http_access_control_request_headers

# 处理OPTIONS命令直接返回,不需要转发到应用服务器
if ($request_method = OPTIONS){
return 200;
}
}
}

配置完之后前端请求的地址可以从http://localhost:8080/user.do变为http://b.com/user.do,这样就完成了应用服务器的虚拟服务器映射,此时cookies需要加在http://b.com这个被调用方域名上,像之前所说的

调用方 - 隐藏跨域解决方案,反向代理(目前最好解决方案)

反向代理的意思是,访问统一域名的两个不同的url,会去到两个不同的服务器

nginx目录下,新建vhost文件夹,新增配置a.com.conf,host已做映射

1
2
# host配置虚拟地址,模拟线上地址
127.0.0.1 a.com

配置nginx.conf

1
2
# nginx.conf 在http块区域里最后面增加
include vhost/*.conf;

nginx.conf目录下新建一个vhost文件夹,新建a.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# a.com 
server {
listen 80;
server_name a.com;

location / {
# 本地客户端代理
proxy_pass http://localhost:8081/;
}

location ~ .*\.do$ {
# 注意,使用了正则匹配代理路径就不能包含uri,即/
proxy_pass http://localhost:3000;
}
}

现在所有的.do请求都会经过3000端口的这个代理

开启nginx(ubuntu下)

1
nginx -c /etc/nginx/nginx.conf

​ 就可以通过a.coma.com/user.do就可以同时访问客户端和后台应用服务器,解决跨域问题

​ 这样解决的最大区别是,请求的地址,之前必须是一个绝对地址,现在是一个相对地址。比如之前请求的地址是http://localhost:3000/users.do,现在只需要请求/users.do即可

​ 这样的解决方法,之前前后端做的跨域设置都不用做,因为现在经过nginx反向代理后,两个地址都指向同一个域名,无跨域问题,cookie也能够发送到后台应用服务器~