前后端分离与跨域问题:原理、解决方案及实践

cuixiaogang

引言

随着前后端分离技术的普及,跨域问题成为开发中常见的挑战。本文将详细介绍前后端分离的优势、跨域问题的原理以及常见的解决方案。

前后端分离的优势

  • 减轻服务器压力,提升前端性能。
  • 前端和后端解耦,便于开发和维护。
  • 提升用户体验,减少页面刷新。

跨域问题的原理

  • 跨域问题的根本原因是什么?
    因为浏览器受到同源策略的限制,当前域名只能请求同域下xhr服务的属性。

  • 什么叫做同源策略?
    就是不同的域名, 不同端口, 不同的协议不允许共享资源的,保障浏览器安全。同源策略是针对浏览器设置的门槛。如果绕过浏览就能实现跨域访问。

跨域问题的解决方案

修改浏览器配置

既然跨域问题的根本原因是浏览器的限制,那么就可以配置浏览器来规避这个问题

1
"C:\ProgramFiles(x86)\Google\Chrome\Application\chrome.exe" --disable-web-security--user-data-dir

以Google Chrome为例,浏览器以上面这种命令打开,就解除了同源限制。

使用jsonp

这是一个JQuery中正常的AJAX请求的代码片段

1
2
3
4
5
6
7
8
9
10
11
$("#demo1").click(function(){
$.ajax({
url : 'http://www.tpadmin.top/Index/Test/crossDomain',
data : {},
type : 'get',
success : function (res) {
//No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1' is therefore not allowed access. 在执行时候报出的错误,这代表了跨域错误
alert(res);
}
});
});

跨域请求失败案例

那么,如果在JQuery中的使用JSONP的AJAX请求呢,看下面的示例

1
2
3
4
5
6
7
8
9
10
11
$("#demo2").click(function(){
$.ajax({
url : 'http://www.tpadmin.top/Index/Test/crossDomain',
data : {},
type : 'get',
dataType : 'jsonp',
success : function (res) {
alert(res);
}
});
});

jsonp请求示例

这时候看到 请求的网址自动变成了

1
http://www.tpadmin.top/Index/Test/crossDomain?callback=jQuery331015214102388989237_1534993962395&_=1534993962396

由于跨域访问的只限制xhr类型的请求(上文中已经说了),所以js中就利用了这一特点,让服务端不在返回的是一个JSON格式的数据,而是返回一段JS代码,将JSON的数据以参数的形式传递到这个函数中,而函数的名称就是callback参数的值,所以还需要修改服务端的代码,代码如下

1
2
3
4
5
6
7
8
<?php
$callback = isset($_GET['callback'])?$_GET['callback']:'';
if (!empty($callback)) {
$arr = ['code' => 200, 'name' => 'cui'];
$data = json_encode($arr);
exit($callback . '(' . $data . ')');
}
?>

jsonp请求示例

OK,现在问题解决了,但是JSONP存在着诸多限制,比如:

  • JSONP只支持GET请求,什么?你要提交表单,sorry,此路不通
  • 它只支持跨域HTTP请求

这些问题让很多人不得不放弃它,所以出现了下面的解决办法。

CORS规范

CORS基础使用

回归问题本质,跨域问题为什么会产生,上面已经说了,是由于浏览器的限制,那么在执行过程中有什么不同,下面两张度分析一下(主要看请求头的部分):

这是非跨域请求
非跨域请求
这是跨域请求
跨域请求

这时发现跨域访问的请求头中存在Origin的字段,用来记录当前的访问域名,可以在服务端增加一个响应头Access-Control-Allow-Origin来告诉浏览器服务端允许它获取就可以了。

  • 配置允许单个域名访问:
1
2
3
4
5
<?php
header('Access-Control-Allow-Origin:http://127.0.0.1');
$arr = ['code' => 200, 'name' => 'cui'];
echo $data = json_encode($arr);
?>

配置允许多个域名访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$requestHeader = getallheaders();
$origin = isset($requestHeader['Origin'])?$requestHeader['Origin']:'';
switch ($origin) {
case 'http://127.0.0.1':
header('Access-Control-Allow-Origin:http://127.0.0.1');
break;
case 'http://localhost':
header('Access-Control-Allow-Origin:http://localhost');
break;
default:
break;
}
$arr = ['code' => 200, 'name' => 'cui'];
echo $data = json_encode($arr);
//注意,不支持下面这种写法
//header('Access-Control-Allow-Origin:http://localhost,http://127.0.0.1');
?>

配置允许所有域名访问:

1
2
3
4
5
<?php
header('Access-Control-Allow-Origin:*');
$arr = ['code' => 200, 'name' => 'cui'];
echo $data = json_encode($arr);
?>

到这里,其实已经结束了,但还有一些其他的特殊情况

  • 请求方法不是GET、HEAD、POST
  • 请求头中存在自定义头
  • Content-Type不是text/plain、multipart/form-data、application/x-www-form-urlencoded
  • 服务端需要获取到客户端的Cookie

CORS应对“非简单请求”(预检请求)

为了测试各种限制,再来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$("#demo1").click(function(){
$.ajax({
url : 'http://cui.tpadmin.top/crossDomain.php',
data : {},
type : 'PUT',
contentType : 'application/json',
header: {
token:'asdfgqwerttyyazxcvbvb'
},
success : function (res) {
alert(res);
}
});
});

非简单请求跨域访问

虽然在服务端加入了Access-Control-Allow-Origin响应头,但是如果出现上面所说的情况时,需要做一些特殊的设置,修改服务端代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
//这里增加了两行代码
header('Access-Control-Allow-Headers:Content-Type');
header('Access-Control-Allow-Methods:PUT');
$requestHeader = getallheaders();
$origin = isset($requestHeader['Origin'])?$requestHeader['Origin']:'';
switch ($origin) {
case 'http://127.0.0.1':
header('Access-Control-Allow-Origin:http://127.0.0.1');
break;
case 'http://localhost':
header('Access-Control-Allow-Origin:http://localhost');
break;
default:
break;
}
$arr = ['code' => 200, 'name' => 'cui'];
echo $data = json_encode($arr);
?>

非简单请求跨域访问

这里需要区分一下简单请求模式与非简单请求模式:

  • 请求方法只能为GET、HEAD、POST
  • 请求头中无自定义头
  • Content-Type必须为text/plain、multipart/form-data、application/x-www-form-urlencoded

符合以上条件的为简单请求,否则为非简单请求。非简单请求中,浏览器会默认发送两条请求,第一条为预检请求(OPTION),第二条为AJAX的请求,出于服务器的性能考量,一般需要将预检命令进行缓存,而不是每次都执行预检请求,可以修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
header('Access-Control-Allow-Headers:Content-Type');
header('Access-Control-Allow-Methods:PUT');
//看这里
header('Access-Control-Max-Age:3600');
$requestHeader = getallheaders();
$origin = isset($requestHeader['Origin'])?$requestHeader['Origin']:'';
switch ($origin) {
case 'http://127.0.0.1':
header('Access-Control-Allow-Origin:http://127.0.0.1');
break;
case 'http://localhost':
header('Access-Control-Allow-Origin:http://localhost');
break;
default:
break;
}
$arr = ['code' => 200, 'name' => 'cui'];
echo $data = json_encode($arr);
?>

预检请求

这次请求了两次,发现第二次请求没有发送预检请求,新增加的代码代表允许缓存的时间(3600S)。

CORS应对Cookie、自定义头等情况

除了以上的各种配置外,还有很多其他的比如,比如

1
2
3
4
5
<?php
//支持Cookie
header('Access-Control-Allow-Credentials:true');
//支持自定义头
header('Access-Control-Allow-Headers:token,Content-Type,...');​

使用nginx或apache的配置

上面的例子实现了跨域访问,但是如果不想在代码中修改,还有其他的方法,比如可以直接修改服务软件(nginx、apache)

  • Apache的配置(想使用Header的话,需要加载mod_headers.so这个模块)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<VirtualHost *:80>
ServerName localhost
ServerAlias localhost
DocumentRoot "${INSTALL_DIR}/www"

Header always set Access-Control-Allow-Header "PUT"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "3600"
#这里设置的为全匹配
Header always set Access-Control-Allow-Origin "expr=%{req:origin}"
#这里设置的为全匹配
Header always set Access-Control-Allow-Headers "expr=%{req:Access-Control-Allow-Headers}"

<Directory "${INSTALL_DIR}/www/">
Options +Indexes +Includes +FollowSymLinks +MultiViews
AllowOverride All
Require local
</Directory>
</VirtualHost>
  • nginx配置
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
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
#支持其他请求
add_header Access-Control-Allow-Methods PUT;
#设置预检请求的缓存
add_header Access-Control-Max-Age 3600;
#允许Cookie
add_header Access-Control-Allow-Credentials true;
#这里最好做判断,怕麻烦的话就写*,但是不建议
if ($http_origin = http://localhost){
add_header Access-Control-Allow-Origin http://localhost;
}
if ($http_origin = http://127.0.0.1){
add_header Access-Control-Allow-Origin http://127.0.0.1;
}
#为了方便,这样写了
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
if ($request_method = OPTIONS){
return 200;
}
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

其他方法

  • 比如使用nginx代理,比如客户端A域名访问服务端B域名,可以在A域名下配置代理服务重定向到B域名,逻辑上避开了浏览器的跨域操作,所以是可以实现跨域访问的。

总结

本文介绍了前后端分离的优势以及跨域问题的原理和解决方案。在实际开发中,推荐使用CORS解决跨域问题,并注意安全性配置。

放学作业

Q:前端服务A域名、API服务端B域名、GO程序自启动监听C域名/IP,它们的逻辑架构为:

  • 其中的B域名使用nginx代理将所有请求重定向到C域名下
  • A域名通过JS请求B域名的API数据,渲染页面数据

那么,在这种情况下,应该将CORS配置在那个服务下?

  • B域名下
  • C域名下
  • B/C域名下都可以