高可用

什么是高可用

高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。如果一个系统能够一直提供服务,那么这个可用性则是百分之百,但是天有不测风云。所以我们只能尽可能的去减少服务的故障。

解决的问题

在生产环境上很多时候是以Nginx做反向代理对外提供服务,但是一天Nginx难免遇见故障,如:服务器宕机。当Nginx宕机那么所有对外提供的接口都将导致无法访问。

虽然我们无法保证服务器百分之百可用,但是也得想办法避免这种悲剧,今天我们使用keepalived来实现Nginx的高可用。

双机热备方案

这种方案是国内企业中最为普遍的一种高可用方案,双机热备其实就是指一台服务器在提供服务,另一台为某服务的备用状态,当一台服务器不可用另外一台就会顶替上去。

keepalived是什么?

Keepalived软件起初是专为LVS负载均衡软件设计的,用来管理并监控LVS集群系统中各个服务节点的状态,后来又加入了可以实现高可用的VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)功能。因此,Keepalived除了能够管理LVS软件外,还可以作为其他服务(例如:Nginx、Haproxy、MySQL等)的高可用解决方案软件

故障转移机制

Keepalived高可用服务之间的故障切换转移,是通过VRRP 来实现的。
在 Keepalived服务正常工作时,主 Master节点会不断地向备节点发送(多播的方式)心跳消息,用以告诉备Backup节点自己还活着,当主 Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主 Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的 IP资源及服务。而当主 Master节点恢复时,备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。

实现过程

安装keepalived

1
sudo apt install keepalived

修改主机(192.168.16.128)keepalived配置文件

yum方式安装的会生产配置文件在/etc/keepalived下:

1
vi keepalived.conf

keepalived.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
#检测脚本
vrrp_script chk_http_port {
script "/usr/local/src/check_nginx_pid.sh" #心跳执行的脚本,检测nginx是否启动
interval 2 #(检测脚本执行的间隔,单位是秒)
weight 2 #权重
}
#vrrp 实例定义部分
vrrp_instance VI_1 {
state MASTER # 指定keepalived的角色,MASTER为主,BACKUP为备
interface ens33 # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡
virtual_router_id 66 # 虚拟路由编号,主从要一直
priority 100 # 优先级,数值越大,获取处理请求的优先级越高
advert_int 1 # 检查间隔,默认为1s(vrrp组播周期秒数)
#授权访问
authentication {
auth_type PASS #设置验证类型和密码,MASTER和BACKUP必须使用相同的密码才能正常通信
auth_pass 1111
}
track_script {
chk_http_port #(调用检测脚本)
}
virtual_ipaddress {
192.168.16.130 # 定义虚拟ip(VIP),可多设,每行一个
}
}

virtual_ipaddress 里面可以配置vip,在线上通过vip来访问服务。

interface需要根据服务器网卡进行设置通常查看方式ip addr

authentication配置授权访问后备机也需要相同配置

修改备机keepalived配置文件

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
#检测脚本
vrrp_script chk_http_port {
script "/usr/local/src/check_nginx_pid.sh" #心跳执行的脚本,检测nginx是否启动
interval 2 #(检测脚本执行的间隔)
weight 2 #权重
}
#vrrp 实例定义部分
vrrp_instance VI_1 {
state BACKUP # 指定keepalived的角色,MASTER为主,BACKUP为备
interface ens33 # 当前进行vrrp通讯的网络接口卡(当前centos的网卡) 用ifconfig查看你具体的网卡
virtual_router_id 66 # 虚拟路由编号,主从要一直
priority 99 # 优先级,数值越大,获取处理请求的优先级越高
advert_int 1 # 检查间隔,默认为1s(vrrp组播周期秒数)
#授权访问
authentication {
auth_type PASS #设置验证类型和密码,MASTER和BACKUP必须使用相同的密码才能正常通信
auth_pass 1111
}
track_script {
chk_http_port #(调用检测脚本)
}
virtual_ipaddress {
192.168.16.130 # 定义虚拟ip(VIP),可多设,每行一个
}
}

检测脚本:

1
2
3
4
5
6
7
8
9
#!/bin/bash
#检测nginx是否启动了
A=`ps -C nginx --no-header |wc -l`
if [ $A -eq 0 ];then #如果nginx没有启动就启动nginx
systemctl start nginx #重启nginx
if [ `ps -C nginx --no-header |wc -l` -eq 0 ];then #nginx重启失败,则停掉keepalived服务,进行VIP转移
killall keepalived
fi
fi

脚本授权:chmod 775 check_nginx_pid.sh

说明:脚本必须通过授权,不然没权限访问啊,在这里我们两条服务器执行、VIP(virtual_ipaddress:192.168.16.130),我们在生产环境是直接通过vip来访问服务。

模拟nginx故障:

修改两个服务器默认访问的Nginx的html页面作为区别。

首先访问192.168.16.130,通过vip进行访问,页面显示192.168.16.128;说明当前是主服务器提供的服务。

这个时候192.168.16.128主服务器执行命令:

systemctl stop nginx; #停止nginx

再次访问vip(192.168.16.130)发现这个时候页面显示的还是:192.168.16.128,这是脚本里面自动重启。

现在直接将192.168.16.128服务器关闭,在此访问vip(192.168.16.130)现在发现页面显示192.168.16.129这个时候keepalived就自动故障转移了,一套企业级生产环境的高可用方案就搭建好了。

keepalived中还有许多功能比如:邮箱提醒等

动静分离

静态资源部署在nginx服务器

nginx来当做静态资源服务器

来自80端口的请求会去nginx服务器根路径下的data文件夹查找

http://xxx.xxx.xxx.xx/static/a/logo.png 会请求根路径下 /data/static/a/logo.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
include mime.types;
default_type application/octet-stream;


server {
listen 80;
server_name xxx.xxx.xxx.xxx;

#charset koi8-r;

#access_log logs/host.access.log main;

location /static/ {
root /data/;
}

}
}

转发到静态资源服务器

转发时使用负载均衡配置

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
http {
include mime.types;
default_type application/octet-stream;
upstream stack_pools {
server 172.25.254.134:80 weight=5;
}
upstream dynamic_pools {
server 172.25.254.135:80 weight=5;
}
server {
listen 80;
server_name xxx.xxx.xxx.xx;
location / {
root html;
index index.html index.htm;
proxy_pass http://dynamic_pools;
}
location /image/ {
proxy_pass http://stack_pools;
}
location /dynamic/ {
roxy_pass http://dynamic_pools;
}
}
}

负载均衡

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
upstream myServer{
ip_hash;
server localhost:1111 weight=10;
server localhost:2222;
fair;
}
listen port;
server_name xxx.xxx.xxx.xx;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
proxy_pass http://myServer;
}
}

轮询策略

每个请求按时间的顺序逐一分配到不同德尔服务器中,如果服务器挂了,可以自动剔除。

权重策略

权重越高,被分配的请求越多

ip hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个服务器后,可以解决session问题。

fair 第三方

根据请求的响应时间来分配,响应时间短的先分配

反向代理

  • proxy_pass 包含 URI 路径(如 /api):

    无论末尾是否有斜杠,均删除 location 匹配部分,拼接剩余路径。

    例:location /webapp + proxy_pass http://xxx/api → /api/剩余路径。

  • proxy_pass 不包含 URI 路径(如 http://xxx):

    末尾无斜杠 → 直接拼接完整请求 URI(http://xxx/完整路径)。

    末尾有斜杠 → 替换 location 匹配部分,拼接剩余路径(http://xxx/剩余路径)。

简单反向代理

把本机 80 端口的请求转发到其他的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
include mime.types;
default_type application/octet-stream;

server {
listen 80;
server_name xxx.xxx.xxx.xxx;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root xxx.xxx.xxx.xxx:port;
index index.html index.htm;
}

}
}

通过正则匹配路径转发

根据路径中不同字段转,把 80 端口的请求,转发到不同的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http {
include mime.types;
default_type application/octet-stream;

server {
listen 80;
server_name xxx.xxx.xxx.xxx;

location ~ /api/ {
proxy_pass xxx.xxx.xxx.xxx:port;
}
location ~ /server/ {
proxy_pass xxx.xxx.xxx.xxx:port;
}

}
}

配置文件

位置

1
/etc/nginx/nginx.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
27
...              #全局块

events { #events块
...
}

http #http块
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
location [PATTERN]
{
...
}
}
server
{
...
}
... #http全局块
}

全局块

1
2
# 值越大,可支持的并发数量越多
worker_processes 1

events块

主要涉及Nginx服务器和用户的网络连接

http块

最常用的配置,代理,缓存,日志定义等绝大多数功能和第三方模块的配置都在这里

http全局块

http全局配置包括文件引入,MIME-TYPE定义,日志自定义,链接超时时间,单链接请求上限等

server块

这块和虚拟主机有密切的关系,虚拟主机从用户的角度看,一台独立的硬件主机是完全一样的,该技术的产生是为了节约互联网服务器硬件成本

每个http块又可以包括多个server块,每个server块就相当于一个虚拟主机

每个server块也分为全局server块,以及可以同时包含多个location块

1
2
3
4
5
6
7
8
server {

# 监听80端口
listen 80;

# 主机名称
server_name localhost;
}
全局server块

最常见的配置是本虚拟主机的监听配置和本虚拟主机的名称或IP配置。

location块

主要作用是基于Nginx服务器接受到的请求字符串(例如,server_name/uri-string),对虚拟主机名称(也可以是ip别名)之外的字符串(例如 前面的uri-string)进行匹配,对待定的请求进行处理,地址定向,数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行

详细配置

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
########### 每个指令必须有分号结束。#################
#user administrator administrators; #配置用户或者组,默认为nobody nobody。
#worker_processes 2; #允许生成的进程数,默认为1
#pid /nginx/pid/nginx.pid; #指定nginx进程运行文件存放地址
error_log log/error.log debug; #制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别以此为:debug|info|notice|warn|error|crit|alert|emerg
events {
accept_mutex on; #设置网路连接序列化,防止惊群现象发生,默认为on
multi_accept on; #设置一个进程是否同时接受多个网络连接,默认为off
#use epoll; #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
worker_connections 1024; #最大连接数,默认为512
}
http {
include mime.types; #文件扩展名与文件类型映射表
default_type application/octet-stream; #默认文件类型,默认为text/plain
#access_log off; #取消服务日志
log_format myFormat '$remote_addr–$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for'; #自定义格式
access_log log/access.log myFormat; #combined为日志格式的默认值
sendfile on; #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
sendfile_max_chunk 100k; #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
keepalive_timeout 65; #连接超时时间,默认为75s,可以在http,server,location块。

upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #热备
}
error_page 404 https://www.baidu.com; #错误页
server {
keepalive_requests 120; #单连接请求上限次数。
listen 4545; #监听端口
server_name 127.0.0.1; #监听地址
location ~*^.+$ { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
#root path; #根目录
#index vv.txt; #设置默认页
proxy_pass http://mysvr; #请求转向mysvr 定义的服务器列表
deny 127.0.0.1; #拒绝的ip
allow 172.18.5.54; #允许的ip
}
}
}

location匹配优先级

匹配优先级从上之下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location = / {
#规则A
}
location = /login {
#规则B
}
location ^~ /static/ {
#规则C
}
location ~ .(gif|jpg|png|js|css)$ {
#规则D
}
location ~* .png$ {
#规则E
}
location / {
#规则F
}

常用命令

使用nginx命令需要进入到nginx可执行文件目录 /usr/sbin/

nginx document

查看版本

1
nginx -v

启动nginx

1
./nginx

快速关闭

1
nginx -s stop

安全关闭

1
nginx -s quit

重新加载配置文件

1
nginx -s reload

nginx安装

命令安装

ubuntu

1
2
sudo apt update
sudo apt install nginx

安装成功后会默认启动在80端口,直接访问会显示nginx主页

查看可执行文件

1
which nginx

查看安装位置

1
whereis nginx

修改防火墙规则

UFW 全称为Uncomplicated Firewall,是Ubuntu 系统上默认的防火墙组件, 为了轻量化配置iptables 而开发的一款工具。

启用防火墙

1
sudo ufw enable

允许80端口访问

1
sudo ufw allow 80

基本概念

Nginx是什么

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器。最高支持50000的并发链接。

正向代理

正向代理的用途:

(1)访问原来无法访问的资源,如google

(2)可以做缓存,加速访问资源

(3)对客户端访问授权,上网进行认证

(4)代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息

反向代理

反向代理(Reverse Proxy)实际运行方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器

反向代理的作用:

(1)保证内网的安全,阻止web攻击,大型网站,通常将反向代理作为公网访问地址,Web服务器是内网

(2)负载均衡,通过反向代理服务器来优化网站的负载

负载均衡

增加服务器的数量,在大量请求的时候,把请求平均分发到不同的服务器

动静分离

在请求资源的时候,区分静态资源(js,html.css)和动态资源(serverlet,jsp),把不同资源的请求转发到不同的服务器

useEffect指南

每次渲染时,UseEffect 都是一个新的自己

每次重新渲染的时,useEffect 都会重新执行,而且如果没有添加依赖项数组的时候,每个 useEffect 都是全新的

而且 useEffect 内部用到的变量都是属于,当前此次执行时捕获到的变量,所以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

function handleAlertClick() {
setTimeout(() => {
alert("You clicked on: " + count);
}, 3000);
}

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}

点击 alert 按钮之后快速点击 click 按钮,在定时器结束之后并不会弹出修改后的值,而是弹出在点击 alert 时 useEffect 捕获到的状态

如果你想读取一个过去或是未来的值,你可以用一个 Ref 去保存,因此对数据的操作不是在 react 的默认行为中,这样虽然显得代码不够干净,但可以确认是需要这样做。

useEffect 中的清理

React 只会在浏览器绘制后运行 effects。这使得你的应用更流畅因为大多数 effects 并不会阻塞屏幕的更新。Effect 的清除同样被延迟了。上一次的 effect 会在重新渲染后被清除

执行的过程应该是: 渲染新 UI -> 清除旧的 Effect -> 执行新的 Effect

清除函数只能读取到在旧的 Effect 执行时捕获到的变量

Effect 依赖

React 它统一描述了初始渲染和之后的更新,React 会根据我们当前的 props 和 state 同步到 DOM,useEffect 使你能够根据 props 和 state 同步 React tree 之外的东西,也包括常说的副作用。

如果你试图写一个 effect 会根据是否第一次渲染而表现不一致,可能违背了 React 初衷

但是如果诚实的告诉了 Effect 依赖,还可能存在问题:

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

由于依赖项的改变定时器被清除

有两种方法可以解决,使用 setState 回调函数,或者使用 useReducer, 如果 useReducer 还需要依赖其他的内部变量,可以吧 useReducer 放到函数组建内部定义

有的时候可能会觉得一个函数不会变换,所以没有加到依赖中,但事实上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SearchResults() {
const [query, setQuery] = useState("react");

function getFetchUrl() {
return "https://hn.algolia.com/api/v1/search?query=" + query;
}

async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}

useEffect(() => {
fetchData();
}, []);
}

函数依赖的参数可能在其他的方法中变化

简单处理的话可以吧相关的函数都放到 useEffect 中,而依赖项只是简单的 query

另外也可以吧 getFetchUrl 放到组件外部,把 query 当做参数传入,或者使用 useCallback 包裹 getFetchUrl

竞态问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Article extends Component {
state = {
article: null,
};
componentDidMount() {
this.fetchData(this.props.id);
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id);
}
}
async fetchData(id) {
const article = await API.fetchArticle(id);
this.setState({ article });
}
// ...
}

如果 componentDidUpdate 中的请求比 componentDidMount 中的请求慢,那么更新中的请求数据会被初始化的请求数据覆盖

我们想做的是,可以打断旧的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Article({ id }) {
const [article, setArticle] = useState(null);

useEffect(() => {
let didCancel = false;

async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
}

跨域CORS

cors是什么

由于浏览器的同源策略,不允许访问非同源的资源,cors的目的就是解决这个问题,也就是常说的跨域访问。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

1.请求方法是以下三种方法之一: HEAD GET POST
2.HTTP的头信息不超出以下几种字段: Accept | Accept-Language | Content-Language | Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

1
2
3
4
5
6
7
fetch("http://xxx",{
headers:{
'content-type':'application/json'
}
method:'POST',
body: JSON.stringify(body)
})

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

Origin: http://xxx

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://xxx
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

  • Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

  • Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

  • Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

  • withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

1
xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: xxx
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

  • Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://xxx
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://xxx可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

1
Access-Control-Allow-Origin: *

如果服务器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

1
2
XMLHttpRequest cannot load http://xxx
Origin http://xxx is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

  • Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

  • Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

  • Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

浏览器的正常请求和回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是”预检”请求之后,浏览器的正常CORS请求。

1
2
3
4
5
6
7
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

1
2
Access-Control-Allow-Origin: http://xxx
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

fetch跨域遇到的问题

如果服务器不支持CORS,fetch提供了三种模式,其中no-cors可以继续访问服务器

fetch的mode配置项有3个值,如下:

  • same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。

  • cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。

  • no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。

no-cors该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其response type为opaque透明的原因。

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信