UDP 端口检测

linux

检测端口是否打开:nc -zuv ip 端口

服务器监听端口:nc -l -u ip 端口(可以发送和接受信息)

客户端检测端口:nc -u ip 端口(可以发送和接受信息)

查看监听的tup端口:ss -ant

查看监听的udp端口:ss -anu

查看所有协议端口:ss -ano

windows

检测端口是否打开:nmap -sU IP -p 端口 -P

ConnectTimeout、SocketTimeout

ConnectTimeout: 建立连接的超时时间,容易理解,TCP 三次握手的时间

SocketTimeout: 数据传输过程中数据包之间间隔的最大时间。
重点说下 SocketTimeout ,有的地方介绍为响应超时时间, 但是有没有想过这个响应超时时间,是指开始响应,还是整个响应完成呢? 如果 服务端 间隔的响应数据(比如 socketTimeout 为3s, 服务端每隔2s响应1个数据,总输出两次,整个耗时会是4s,会触发整个超时异常么?)

参考:https://blog.csdn.net/btlas/article/details/53710854

Idea

功能 IDEA
多点编辑 Alt + Shift + 鼠标点击
Ctrl+w 选中变量,Alt+j选中要选择的多个相同变量
Alt + Enter (双引号内): Inject Inject Language ,方便 的 JSON 插入, 正则直接的校验
Shift + F7 : 一行多个方法,让你选中进入某个方法(不必要每个方法再进一次了,可以方便的来跳过gettter,setter)
方法参数提示 Ctrl+p
Quick Text Search Ctrl + Shift + F (Preview界面)
定位文件/类 Ctrl + Shift + N
Ctrl + N
类内部导航 Ctrl + F12 / Alt + 7
窗口最大化 Ctrl + Shift + F12
项目导航 Alt + 1 (Alt + [1-9]好像都有对应的视图显示,可以尝试)
代码生成get/set/toString等 Alt + Insert
代码补全 Ctrl + J
(psvm/sout/souf)
补全 Options->Keymap->copy->Main Menu->Code->Complete Code->
导入包 Alt + Enter or Ctrl + Alt + O
快速修复 Alt + Enter
显示类/方法说明 Ctrl + Q
调试单步进入 F7
调试单步跳出 F8
跳过 F9
执行选中语句 Alt + F8
变量重命名 Shift + F6
查看实现 Ctrl + Alt + B
查看方法在哪里被使用 Alt+F7/Ctrl + B
代码Back/Forward Ctrl + Alt + Left/Right
代码包裹/Surround With Ctrl + Alt + T
格式化代码 Ctrl + Alt + L
切换项目窗口 Ctrl+Shift+[
—————————————- ——————————————————————————–

Cassandra 数据库使用小记

Cassandra 数据库使用小记

一、安装与配置

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
# cassadra
wget http://mirrors.shu.edu.cn/apache/cassandra/3.11.3/apache-cassandra-3.11.3-bin.tar.gz
tar -zxvf apache-cassandra-3.11.3-bin.tar.gz -C /usr/local/

# 建立软链,方便升级
cd /usr/local/
ln -s apache-cassandra-3.11.3 cassandra

# 添加到环境变量
cat << EOF >> /etc/profile
export PATH=/usr/local/cassandra/bin:\$PATH
EOF

source /etc/profile

# 修改配置,方便内网连接
vim /usr/local/cassandra/conf/cassandra.yaml
> cluster_name: 'Kong-dev-cluster'
> - seeds: "172.18.1.16"
> listen_address: 172.18.1.16
> rpc_address: 172.18.1.16

# 建立 cassandra 用户(cassandra建议非root用户运行)
groupadd cassandra
useradd -g cassandra cassandra
chown -R cassandra:cassandra /usr/local/cassandra/

# 启动 (默认非 daemon 模式,可配置人 supervisor)
cassandra -f

# or 配置 supervisor
cat << EOF > /usr/local/deploy/supervisord/conf/cassandra.conf
[program:cassandra]
user=cassandra
command=cassandra -f
autostart=false
autorestart=unexpected
stdout_logfile=/usr/local/deploy/log/cassandra.log
stderr_logfile=/usr/local/deploy/log/cassandra.err
EOF

二、命令行使用

技巧:cqlsh 提供的命令行是具有智能提示的,输入命令的部分字母,按Tab建可以出发,再CREATE KEYSPACE 这条命令中可以深切的感受到方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cqlsh 172.18.10.217
cqlsh> show version;
cqlsh> show host;
cqlsh> desc cluster;
cqlsh> desc keyspaces;
cqlsh> CREATE KEYSPACE test_keyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
--(实际输入:C<Tab> k<Tab> test_keyspace w<Tab> s<Tab> <Tab> 1};)
cqlsh> desc keyspace test_keyspace;
cqlsh> use test_keyspace;
cqlsh:test_keyspace> desc tables;
cqlsh:test_keyspace> create table abc ( id int primary key, name varchar, age int );
cqlsh:test_keyspace> desc table abc;
cqlsh:test_keyspace> DROP TABLE abc ; -- abc 只输入 a, <Tab> 也可以自动补全哦
cqlsh:test_keyspace> drop KEYSPACE test_keyspace ;

更多命令请参考 官方文档

三、数据类型(附)

CQL类型 对应Java类型 描述
ascii String ascii字符串
bigint long 64位整数
blob ByteBuffer/byte[] 二进制数组
boolean boolean 布尔
counter long 计数器,支持原子性的增减,不支持直接赋值
decimal BigDecimal 高精度小数
double double 64位浮点数
float float 32位浮点数
inet InetAddress ipv4或ipv6协议的ip地址
int int 32位整数
list List 有序的列表
map Map 键值对
set Set 集合
text String utf-8编码的字符串
timestamp Date 日期
uuid UUID UUID类型
timeuuid UUID 时间相关的UUID
varchar string text的别名
varint BigInteger 高精度整型

wrk 压力测试

wrk 压力测试

一、安装

1
2
3
4
git clone --depth=1 https://github.com/wg/wrk 
cd wrk
make
ln -sf /root/tools/wrk/wrk /usr/local/bin/wrk

查看下版本以及命令帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ wrk -v
wrk 4.1.0 [epoll] Copyright (C) 2012 Will Glozer
Usage: wrk <options> <url>
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use

-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
--latency Print latency statistics # 加上此选项可输出响应时间的分布情况
--timeout <T> Socket/request timeout # wrk 默认超时时间是1秒,用此参数设置超时时间
-v, --version Print version details

Numeric arguments may include a SI unit (1k, 1M, 1G)
Time arguments may include a time unit (2s, 2m, 2h)

二、使用

1. 先来一个简单的测试

1
wrk -t12 -c100 -d30s http://www.baidu.com

30秒钟结束以后可以看到如下输出:

1
2
3
4
5
6
7
8
9
Running 30s test @ http://www.baidu.com
12 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.58ms 155.96ms 1.66s 90.27% # 可以理解为响应时间, 平均值, 标准偏差, 最大值, 正负一个标准差占比
Req/Sec 203.95 150.79 1.55k 76.91% # 每个线程每秒钟的完成的请求数, 均值...
70847 requests in 30.02s, 1.01GB read # 30秒钟总共完成请求数和读取数据量.
Socket errors: connect 0, read 499, write 0, timeout 4 # 错误统计, 499个读错误, 4个超时
Requests/sec: 2359.99 # 总共平均每秒钟完成 2359 个请求
Transfer/sec: 34.40MB # 每秒钟读取 34.4M 数据

wrk 默认超时时间是1秒,可以使用T参数设置,如设置超时为10s:

1
wrk -t12 -c100 -T10s -d30s http://www.baidu.com

输出响应时间分布:

1
wrk -t12 -c100 -T10s -d30s --latency http://www.baidu.com

2. 结合Lua脚本测试

HTTP请求通常不会这么简单,通常还有POST, Header, body 等,可以结合Lua脚本来测试。

建立一个 post.lua文件:

1
2
3
wrk.method = "POST"
wrk.body = "foo=bar&baz=quux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencode"

然后执行:

1
wrk -t12 -c100 -d10s -T30s --script=post.lua --latency http://www.baidu.com

查看 wrk.lua 的源码,https://github.com/wg/wrk/blob/master/src/wrk.lua, 可以看出有以下属性:

1
2
3
4
5
6
7
8
9
10
local wrk = {
scheme = "http",
host = "localhost",
port = nil,
method = "GET",
path = "/",
headers = {},
body = nil,
thread = nil,
}

wrk 可以在lua脚本里添加下面的Hook函数,你可以想象成生命周期,每个生命周期做的事情都不一样, 但是生命周期是有时间顺序的。我们常用一般是 request 和 delay 周期. 另外还提供了以下几个hook函数:

  • setup 线程初始后支持会调用一次

  • init 每次请求发送之前被调用。可以接受 wrk 命令行的额外参数

  • delay 这个函数返回一个数值,在这次请求执行完以后延迟多长时间执行下一个请求,可以对应 thinking time 的场景

  • request 通过这个函数可以每次请求之前修改本次请求体和Header,我们可以在这里写一些要压力测试的逻辑。

  • response 每次请求返回以后被调用,可以根据响应内容做特殊处理,比如遇到特殊响应停止执行测试,或输出到控制台等等。

    1
    2
    3
    4
    5
    6
    function response(status, headers, body)  
    if status ~= 200 then
    print(body)
    wrk.thread:stop()
    end
    end
  • done 在所有请求执行完以后调用, 一般用于自定义统计结果.

    1
    2
    3
    4
    5
    6
    7
    done = function(summary, latency, requests)  
    io.write("------------------------------\n")
    for _, p in pairs({ 50, 90, 99, 99.999 }) do
    n = latency:percentile(p)
    io.write(string.format("%g%%,%d\n", p, n))
    end
    end

wrk 源码中给出的完整示例 ,源码 (这个目录下其他 脚本也可以参考)

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
local counter = 1  
local threads = {}

function setup(thread)
thread:set("id", counter)
table.insert(threads, thread)
counter = counter + 1
end

function init(args)
requests = 0
responses = 0

local msg = "thread %d created"
print(msg:format(id))
end

function request()
requests = requests + 1
return wrk.request()
end

function response(status, headers, body)
responses = responses + 1
end

function done(summary, latency, requests)
for index, thread in ipairs(threads) do
local id = thread:get("id")
local requests = thread:get("requests")
local responses = thread:get("responses")
local msg = "thread %d made %d requests and got %d responses"
print(msg:format(id, requests, responses))
end
end

以通过 lua 实现访问多个 url.

例如这个复杂的 lua 脚本, 随机读取 paths.txt 文件中的 url 列表, 然后访问:

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
counter = 1  

math.randomseed(os.time())
math.random(); math.random(); math.random()

function file_exists(file)
local f = io.open(file, "rb")
if f then f:close() end
return f ~= nil
end

function shuffle(paths)
local j, k
local n = #paths
for i = 1, n do
j, k = math.random(n), math.random(n)
paths[j], paths[k] = paths[k], paths[j]
end
return paths
end

function non_empty_lines_from(file)
if not file_exists(file) then return {} end
lines = {}
for line in io.lines(file) do
if not (line == '') then
lines[#lines + 1] = line
end
end
return shuffle(lines)
end

paths = non_empty_lines_from("paths.txt")

if #paths <= 0 then
print("multiplepaths: No paths found. You have to create a file paths.txt with one path per line")
os.exit()
end

print("multiplepaths: Found " .. #paths .. " paths")

request = function()
path = paths[counter]
counter = counter + 1
if counter > #paths then
counter = 1
end
return wrk.format(nil, path)
end

关于 cookie

有些时候我们需要模拟一些通过 cookie 传递数据的场景. wrk 并没有特殊支持, 可以通过 wrk.headers["Cookie"]="xxxxx"实现.

下面是在网上找的一个取 Response的cookie作为后续请求的cookie :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getCookie(cookies, name)  
local start = string.find(cookies, name .. "=")

if start == nil then
return nil
end

return string.sub(cookies, start + #name + 1, string.find(cookies, ";", start) - 1)
end

response = function(status, headers, body)
local token = getCookie(headers["Set-Cookie"], "token")

if token ~= nil then
wrk.headers["Cookie"] = "token=" .. token
end
end

Supervisor的使用

supervisor

Supervisord 是用 Python 实现的一款非常实用的进程管理工具,supervisord 还要求管理的程序是非 daemon 程序,supervisord 会帮你把它转成 daemon 程序,因此如果用 supervisord 来管理 nginx 的话,必须在 nginx 的配置文件里添加一行设置 daemon off 让 nginx 以非 daemon 方式启动。

1. 安装 与 默认配置

1
2
3
4
5
apt-cache search python-setuptools
sudo apt-get install python-setuptools
easy_install supervisor
supervisord -v
echo_supervisord_conf > /etc/supervisord.conf # 默认的配置 输入 /etc/supvervisord.conf

修改为自己习惯的配置 /etc/supvervisord.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
[unix_http_server]          ; supervisord的unix socket服务配置
file=/usr/local/deploy/supervisord/supervisor.sock ; socket文件的保存目录

[inet_http_server] ; supervisord的tcp服务配置
port=*:9001 ; tcp端口

[supervisord] ; supervisord的主进程配置
logfile=/usr/local/deploy/supervisord/supervisord.log ; 主要的进程日志配置
logfile_maxbytes=50MB ; 最大日志体积,默认50MB
logfile_backups=10 ; 日志文件备份数目,默认10
loglevel=info ; 日志级别,默认info; 还有:debug,warn,trace
pidfile=/usr/local/deploy/supervisord/supervisord.pid ; supervisord的pidfile文件
nodaemon=false ; 是否以守护进程的方式启动
minfds=1024 ; 最小的有效文件描述符,默认1024
minprocs=200 ; 最小的有效进程描述符,默认200

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///usr/local/deploy/supervisord/supervisor.sock ; use a unix:// URL for a unix socket

[include]
files = /usr/local/deploy/supervisord/conf/*.conf

2. 加载自定义配置目录 vim /etc/supervisord.conf

1
2
3
4
5
mkdir -p /usr/local/deploy/supervisord/conf/

;...
[include]
files = /usr/local/deploy/supervisord/conf/*.conf

3. 新建管理的应用

1
2
cd /usr/local/deploy/supervisord/conf/
vim express-service.conf

内容:

1
2
3
4
5
6
[program:express-service]
command=node /home/vagrant/test-service/app.js
autostart=false
autorestart=false
stdout_logfile=/usr/local/deploy/log/express-service.out
stderr_logfile=/usr/local/deploy/log/express-service.err

如果报错,可以使用 supervisorctl tail express-service stdout 来查看具体报错。

另外配置文件可使用:%(directory) 的格式来引用变量。

常见问题:

  1. 直接访问http://127.0.0.1:9001/ 可以查看 Tail -f的日志,通过Nginx代理后则不行;Nginx需要增加以下配置,参考Supervisord inet_http_server behind nginx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    server {
    listen 9000;
    listen [::]:9000;

    server_name server.gmetri.com;

    location / {
    proxy_pass http://127.0.0.1:9001;
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_max_temp_file_size 0;
    proxy_redirect default;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Connection "";
    }
    }

supervisord 管理

启动:supervisord (默认会读/etc/supervisord.conf)或者 指定主配置supervisord -c /etc/supervisord.conf

  • supervisorctl 不跟其他,进入 supervisorctl console, status 可以查看所有任务的状态,uptime等,新增任务需要 update
  • supervisorctl update,根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启。
  • supervisorctl reload,载入最新的配置文件,停止原有进程并按新的配置启动、管理所有进程。
  • supervisorctl stop xxx 停止某一个进程

附 task 配置 Demo

auth-service.conf:

1
2
3
4
5
6
[program:auth-service]
command=java -D"app.id=gateway" -D"env=FAT" -javaagent:/usr/local/skywalking-agent/skywalking-agent.jar -jar /usr/local/deploy/auth-service-exec.jar
autostart=false
autorestart=false
stdout_logfile=/usr/local/deploy/log/auth-service.out
stderr_logfile=/usr/local/deploy/log/auth-service.err

shadowsocks.conf:

1
2
3
4
5
[program:shadowsocks]
command=sslocal -c ~/shadowsocks.json
autostart=true
autorestart=true
user=nobody

附 program 配置

http://www.supervisord.org/configuration.html#program-x-section-example

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
[program:cat]
command=/bin/cat
process_name=%(program_name)s
numprocs=1
directory=/tmp
umask=022
priority=999
autostart=true
autorestart=unexpected
startsecs=10
startretries=3
exitcodes=0,2
stopsignal=TERM
stopwaitsecs=10
stopasgroup=false
killasgroup=false
user=chrism
redirect_stderr=false
stdout_logfile=/a/path
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
stdout_capture_maxbytes=1MB
stdout_events_enabled=false
stderr_logfile=/a/path
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=10
stderr_capture_maxbytes=1MB
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO

附 supervisor 配置说明:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Note: shell expansion ("~" or "$HOME") is not supported. Environment
; variables can be expanded using this syntax: "%(ENV_HOME)s".

[unix_http_server] ; supervisord的unix socket服务配置
file=/tmp/supervisor.sock ; socket文件的保存目录
;chmod=0700 ; socket的文件权限 (default 0700)
;chown=nobody:nogroup ; socket的拥有者和组名
;username=user ; 默认不需要登陆用户 (open server)
;password=123 ; 默认不需要登陆密码 (open server)

;[inet_http_server] ; supervisord的tcp服务配置
;port=127.0.0.1:9001 ; tcp端口
;username=user ; tcp登陆用户
;password=123 ; tcp登陆密码

[supervisord] ; supervisord的主进程配置
logfile=/tmp/supervisord.log ; 主要的进程日志配置
logfile_maxbytes=50MB ; 最大日志体积,默认50MB
logfile_backups=10 ; 日志文件备份数目,默认10
loglevel=info ; 日志级别,默认info; 还有:debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord的pidfile文件
nodaemon=false ; 是否以守护进程的方式启动
minfds=1024 ; 最小的有效文件描述符,默认1024
minprocs=200 ; 最小的有效进程描述符,默认200
;umask=022 ; 进程文件的umask,默认200
;user=chrism ; 默认为当前用户,如果为root则必填
;identifier=supervisor ; supervisord的表示符, 默认时'supervisor'
;directory=/tmp ; 默认不cd到当前目录
;nocleanup=true ; 不在启动的时候清除临时文件,默认false
;childlogdir=/tmp ; ('AUTO' child log dir, default $TEMP)
;environment=KEY=value ; 初始键值对传递给进程
;strip_ansi=false ; (strip ansi escape codes in logs; def. false)

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris ; 如果设置应该与http_username相同
;password=123 ; 如果设置应该与http_password相同
;prompt=mysupervisor ; 命令行提示符,默认"supervisor"
;history_file=~/.sc_history ; 命令行历史纪录

; The below sample program section shows all possible program subsection values,
; create one or more 'real' program: sections to be able to control them under
; supervisor.

;[program:theprogramname]
;command=/bin/cat ; 运行的程序 (相对使用PATH路径, 可以使用参数)
;process_name=%(program_name)s ; 进程名表达式,默认为%(program_name)s
;numprocs=1 ; 默认启动的进程数目,默认为1
;directory=/tmp ; 在运行前cwd到指定的目录,默认不执行cmd
;umask=022 ; 进程umask,默认None
;priority=999 ; 程序运行的优先级,默认999
;autostart=true ; 默认随supervisord自动启动,默认true
;autorestart=unexpected ; whether/when to restart (default: unexpected)
;startsecs=1 ; number of secs prog must stay running (def. 1)
;startretries=3 ; max # of serial start failures (default 3)
;exitcodes=0,2 ; 期望的退出码,默认0,2
;stopsignal=QUIT ; 杀死进程的信号,默认TERM
;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false ; 向unix进程组发送停止信号,默认false
;killasgroup=false ; 向unix进程组发送SIGKILL信号,默认false
;user=chrism ; 为运行程序的unix帐号设置setuid
;redirect_stderr=true ; 将标准错误重定向到标准输出,默认false
;stdout_logfile=/a/path ; 标准输出的文件路径NONE=none;默认AUTO
;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false ; emit events on stdout writes (default false)
;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10 ; # of stderr logfile backups (default 10)
;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false ; emit events on stderr writes (default false)
;environment=A=1,B=2 ; process environment additions (def no adds)
;serverurl=AUTO ; override serverurl computation (childutils)

; The below sample eventlistener section shows all possible
; eventlistener subsection values, create one or more 'real'
; eventlistener: sections to be able to handle event notifications
; sent by supervisor.

;[eventlistener:theeventlistenername]
;command=/bin/eventlistener ; 运行的程序 (相对使用PATH路径, 可以使用参数)
;process_name=%(program_name)s ; 进程名表达式,默认为%(program_name)s
;numprocs=1 ; 默认启动的进程数目,默认为1
;events=EVENT ; event notif. types to subscribe to (req'd)
;buffer_size=10 ; 事件缓冲区队列大小,默认10
;directory=/tmp ; 在运行前cwd到指定的目录,默认不执行cmd
;umask=022 ; 进程umask,默认None
;priority=-1 ; 程序运行的优先级,默认-1
;autostart=true ; 默认随supervisord自动启动,默认true
;autorestart=unexpected ; whether/when to restart (default: unexpected)
;startsecs=1 ; number of secs prog must stay running (def. 1)
;startretries=3 ; max # of serial start failures (default 3)
;exitcodes=0,2 ; 期望的退出码,默认0,2
;stopsignal=QUIT ; 杀死进程的信号,默认TERM
;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false ; 向unix进程组发送停止信号,默认false
;killasgroup=false ; 向unix进程组发送SIGKILL信号,默认false
;user=chrism ; setuid to this UNIX account to run the program
;redirect_stderr=true ; redirect proc stderr to stdout (default false)
;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10 ; # of stdout logfile backups (default 10)
;stdout_events_enabled=false ; emit events on stdout writes (default false)
;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups ; # of stderr logfile backups (default 10)
;stderr_events_enabled=false ; emit events on stderr writes (default false)
;environment=A=1,B=2 ; process environment additions
;serverurl=AUTO ; override serverurl computation (childutils)

; The below sample group section shows all possible group values,
; create one or more 'real' group: sections to create "heterogeneous"
; process groups.

;[group:thegroupname]
;programs=progname1,progname2 ; 任何在[program:x]中定义的x
;priority=999 ; 程序运行的优先级,默认999

; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.

;[include]
;files = relative/directory/*.ini

作者:BlackRun
链接:https://www.jianshu.com/p/5c3922fa97c0
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Lua OpenResty 开发环境搭建

1. 创建目录/usr/servers,以后我们把所有软件安装在此目录

1
2
3
mkdir -p /usr/servers
cd /usr/servers/
ls

2.下载openresty-1.13.6.2.tar.gz并解压

1
2
3
wget https://openresty.org/download/openresty-1.13.6.2.tar.gz
tar -zxvf openresty-1.13.6.2.tar.gz
cd openresty-1.13.6.2/bundle/

3. 安装LuaJIT

1
2
3
cd LuaJIT-2.1-20180420/
ls
make clean && make && make install

4. 下载ngx_cache_purge模块,该模块用于清理nginx缓存

1
2
3
cd /usr/servers/openresty-1.13.6.2/bundle/
wget https://github.com/FRiCKLE/ngx_cache_purge/archive/2.3.tar.gz
tar -xvf 2.3.tar.gz

5. 下载nginx_upstream_check_module模块,该模块用于ustream健康检查

1
2
3
4
cd /usr/servers/ngx_openresty-1.7.7.2/bundle
cd /usr/servers/openresty-1.13.6.2/bundle/
wget https://github.com/yaoweibin/nginx_upstream_check_module/archive/v0.3.0.tar.gz
tar -xvf v0.3.0.tar.gz

6. 安装 openresty

1
2
3
4
yum install openssl-devel pcre-devel zlib-devel -y
cd /usr/servers/openresty-1.13.6.2/
./configure --prefix=/usr/servers --with-http_realip_module --with-pcre --with-luajit --add-module=./bundle/ngx_cache_purge-2.3/ --add-module=./bundle/nginx_upstream_check_module-0.3.0/ -j2
make && make install

7. 目录说明

/usr/servers/luajit :luajit环境,luajit类似于java的jit,即即时编译,lua是一种解释语言,通过luajit可以即时编译lua代码到机器代码,得到很好的性能;
/usr/servers/lualib:要使用的lua库,里边提供了一些默认的lua库,如redis,json库等,也可以把一些自己开发的或第三方的放在这;
/usr/servers/nginx :安装的nginx;

8. 启动nginx

/usr/servers/nginx/sbin/nginx

9. 配置环境

  1. 编辑nginx.conf配置文件
    1
    vim /usr/servers/nginx/conf/nginx.conf
  1. 在http部分添加如下配置
    1
    2
    3
    #lua模块路径,多个之间”;”分隔,其中”;;”表示默认搜索路径,默认到/usr/servers/nginx下找
    lua_package_path "/usr/servers/lualib/?.lua;;"; #lua 模块
    lua_package_cpath "/usr/servers/lualib/?.so;;"; #c模块
  1. 为了方便开发我们在/usr/servers/nginx/conf目录下创建一个lua.conf
    1
    2
    3
    4
    5
    #lua.conf
    server {
    listen 80;
    server_name _;
    }
  1. 在nginx.conf中的http部分添加include lua.conf包含此文件片段
    1
    include lua.conf;
  1. 测试是否正常
    1
    /usr/servers/nginx/sbin/nginx -t

Nginx+Lua 实现灰度发布

一、安装Lua环境及相关库

1. LuaJIT

1
2
3
4
5
6
wget http://luajit.org/download/LuaJIT-2.0.5.tar.gz
tar -zxvf LuaJIT-2.0.5.tar.gz -C /usr/local/src/
cd /usr/local/src/LuaJIT-2.0.5/
make install PREFIX=/usr/local/LuaJIT
export LUAJIT_LIB=/usr/local/LuaJIT/lib
export LUAJIT_INC=/usr/local/LuaJIT/include/luajit-2.0

2. ngx_devel_kit和lua-nginx-module

1
2
3
4
wget https://github.com/simplresty/ngx_devel_kit/archive/v0.3.1rc1.tar.gz
wget https://github.com/openresty/lua-nginx-module/archive/v0.10.13.tar.gz
tar -zxvf v0.3.1rc1.tar.gz -C /usr/local/src/
tar -zxvf v0.10.13.tar.gz -C /usr/local/src/

3. 编译Nginx

1
2
3
4
5
6
wget http://nginx.org/download/nginx-1.15.2.tar.gz
tar -zxvf nginx-1.15.2.tar.gz -C /usr/local/src/
cd /usr/local/src/nginx-1.15.2/
# 以下编译选项,除后两个,其余其实就是yum安装ngnix下nginx -V的默认编译选项
./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie' --add-module=/usr/local/src/ngx_devel_kit-0.3.1rc1 --add-module=/usr/local/src/lua-nginx-module-0.10.13
make -j 4 && make install

4. 加载lua库,加入到ld.so.conf文件

1
2
echo "/usr/local/LuaJIT/lib" >> /etc/ld.so.conf
ldconfig

二、灰度实现

1. 灰度目标

当客户端ip为192.168.56.1时,让其访问新发布的upstream测试实例;其他ip访问时还是老的upstream实例。相当于前者为新发布功能,后者为稳定的生产功能。

2. 灰度准备

  1. 这里使用 memcache 来存储哪些ip要访问新的,哪些ip访问稳定的老的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    yum install memcached 
    memcached -p11211 -u nobody -d
    # 设置 灰度ip
    telnet 127.0.0.1 11211
    set 192.168.56.1 0 0 1
    1
    get 192.168.56.1
    # 安装lua-resty-memcached
    wget https://github.com/openresty/lua-resty-memcached/archive/v0.14.tar.gz
    tar -zxvf v0.14.tar.gz -C /usr/local/src/
    cp -r /usr/local/src/lua-resty-memcached-0.14/lib/resty /usr/local/LuaJIT/share/luajit-2.0.5/
  2. 准备两个服务新上线服务稳定生产服务,这里使用两个tomcat来演示(如果你熟悉node 也可以使用node快速实现两个后台服务)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #测试页面
    vim test.jsp
    1 <%@ page language="java" import="java.util.*" pageEncoding="utf-8" %>
    2 <!DOCTYPE html>
    3 <html lang="en">
    4 <head>
    5 | <meta charset="UTF-8">
    6 | <title>Test Jsp</title>
    7 </head>
    8 <body>
    9 | <%
    10 | | Random rand = new Random();
    11 | | out.println("<h1>Test server</h1>");
    12 | | out.println("<h1>Random number:</h1>");
    13 | | out.println(rand.nextInt(99) + 100);
    14 | %>
    15 </body>
    16 </html>
    # 两个tomcat: tomcat8080, tomcat9090,前者作为新上线服务,后者作为 稳定的生产服务;将上面的test.jsp页面分别放置到两个tomcat的ROOT下;tomcat9090 删除test.jsp中的11行,以示区别。
    # 分别启动两个tomcat
  3. 配置Nginx nginx.conf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    |   location / { 
    | | default_type 'text/html';
    | | content_by_lua_file /opt/app/lua/dep.lua;
    | }
    | location @server{
    | | proxy_pass http://127.0.0.1:9090;
    | }
    | location @server_test{
    | | proxy_pass http://127.0.0.1:8080;
    | }
  4. 编写 /opt/app/lua/dep.lua

    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
    local clientIP = ngx.req.get_headers()["X-Real-IP"]
    if clientIP == nil then
    | clientIP = ngx.req.get_headers()["x_forwarded_for"]
    end
    if clientIP == nil then
    | clientIP = ngx.var.remote_addr
    end
    local memcached = require "resty.memcached"
    local memc, err = memcached:new()
    if not memc then
    | ngx.say("failed to instance memc:", err)
    | return
    end
    local ok, err = memc:connect("127.0.0.1", 11211)
    if not ok then
    | ngx.say("failed to connect: ", err)
    | return
    end
    local res, flags, err = memc:get(clientIP)
    --ngx.say("value key: ", res, clientIP)
    if err then
    | ngx.say("failed to get clientIP ", err)
    end
    if res == "1" then
    | ngx.exec("@server_test")
    | return
    end
    ngx.exec("@server")
  5. 启动Nginx,访问测试

在本机使用 curl http://127.0.0.1/test.jsp 查看效果,返回结果无 Test server 字样

192.168.56.1 访问 http://192.168.56.101(此ip为Nginx 服务所在ip) 查看效果,返回结果有 Test server 字样

三、小结

以上只是简单的针对某个 ip 做的灰度,完整的灰度,还需要企业后台灵活的管理memcache

另外要实现更灵活的灰度,比如根据 Cookie, header 头信息,也可以个根据这种原理来实现。

大白话简记TCP

ISO 定的 OSI 七层模型

协议族五层

TCP 生命周期

tcp连接生命周期

  • 对于建链接的3次握手 主要是初始化Sequence Number的初始值。通信的双方要互相知道对方初始化的Sequence Number(缩写ISN: Initial Sequence Number),所以叫SYN,全称:Synchronize Sequence Number。这个同步序号作为此TCP连接后面数据通信的序号,以保证应用层能处理乱序、重复、丢失的问题。

  • 对于4次挥手 其实你仔细看是2次,因为TCP是全双工的,所以双方都会发出FINACK一次请求和应答。只不过有一方是被动的,所以看上去就像是4次挥手。如果两边同时发出断开的请求,那就会进入到CLOSING的状态,然后到达TIME_WAIT状态。下图是双方同时段连接的示意图:

同时发起断开请求

因为双方同时发起了FIN,都认为自己是FIN发起的主动方,各自都进入FIN_WAIT_1状态,都在等待对方被动发出FIN请求,同时双方都收到对方的FIN请求,于是各自应答对方ACK,对方进入CLOSING状态,这个CLOSING都是在等待前面提到等待对方发出被动的FIN请求,都在互相等待,那就像死锁了,这里的解决方案就是进入CLOSING状态时同时开启TIME_WAIT等待指定的时机后会自动进入CLOSED状态。

下面我们用电话通话的例子来比拟下这里的建立连接3次握手,断开链接4次挥手。

打电话的3次握手

A: 你听到我说话么? – 1. 发往B的一个握手请求。发出问题让对方回答,目的是为了验证自己说的话对方能听到,即验证自己的话筒 -> 对方的听筒这条链路。

B: 我听到你说话,你能听到我说话吗? –2. 发往A的一个握手请求。回答对方的问题,让对方知道他的验证通过。自己再发出问题,目的是为了验证自己说的话对方能听到,也就是:B的话筒 -> A的听筒这条链路是通的。

A: 我也能听到你说话 – 3. 发往B的一个握手请求。回答B的问题,让B知道他说的话自己也能听到。

4次挥手挂电话

A: 我的话说完了,要挂电话了,你说完了么? (发起断开 A -> B 的请求)

B: 好吧(应答下对方的挂掉意思本方已经收到, 这里意味着 A -> B 不会再发起新的请求,只有应答)

….

B: 我也说完了,我也要挂电话了 (发起断开 B -> A链路的请求)

A: 好的 (应答对方,意味B -> A的链路也断开了,整个连接完整断开)

这里要解释的一点是,上面B的连在一起的两句话,为什么不能像建立连接时候的两句话再一次请求里一次说掉呢? 因为bye bye前可能还有话正在说,而hello是对话的开始,之前没有别的话。

几个需要注意的事情

  • 关于建连接SYN超时 试想一下,如果server接到了clientSYN请求,然后应答了ACK-SYNclient掉线,server端没有收到client回来的ACK,那么连接处于一个中间的状态,既没成功,也没失败。于是server端如果在一定时间内没有收到ACK,TCP回重发ACK-SYN。在Linux下,默认重试次数是5次,5次重试的时间间隔分别是:1s, 2s, 4s, 8s, 16s, 32s,所以总共需要1s+2s+4s+8s+16s+32s = 2^6 -1 = 63sTCP才会断开这个连接

  • 关于SYN Flood攻击 一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后就下线,于是服务器默认等63s才会断开连接,这样攻击者重复这个过程就可以把服务器SYN连接队列耗尽,让正常的连接请求不能处理。于是,Linux下给了一个叫tcp_syncookies的参数来应对这个事——当SYN连接队列满了以后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个SYN cookie发回来,然后服务端可以通过cookie来建连接(即使你不在SYN队列中),请注意请先千万别用tcp_syncookies来处理正常的大负载连接的情况。因为syncookies是妥协版的TCP协议,并不严谨。对于正常的请求有三个参数可供调整,第一个是:tcp_synack_retries可以减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 处理不过来就直接拒绝连接。

  • 关于ISN的初始化 ISN是不能用hard code的,不然会有问题——如果连接建立好始终使用1来作ISN,client发了30segment过去,但是网络断了,于是client重连,又用1ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,clientSequence Number可能是3,而server端认为client的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒ISN做加一操作,知道超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP segment在网络上的存货时间不会超过Maximun Segment Lifetime(MSL),所以,只要MSL的值小于4.55小时,那么我们就不会重用到ISN

  • 关于MSLTIME_WAITTCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置是2*MSL(RFC793定义MSL分钟,Linux设置为30s),为什么要有TIME_WAIT,不直接转成CLOSED状体呢?主要有两个原因:1) TIME_WAIT确保有足够的时机让对方收到ACK,如果被动关闭的那方没有收到ACK,就会触发被动端重发FIN,一来一去正好2个MSL 2) 有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)

  • 关于TIME_WAIT数量太多 从上面的描述我们可以知道,TIME_WAIT是一个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT就会太多,这也会消耗很多系统资源。只要搜一下,就会发现多数方式会教你设置两个参数,一个tcp_tw_reuse,另一叫tcp_tw_recycle,这两个参数默认是关闭的后者recycle比前者更激进,reuse要温柔一些。另外,如果使用tcp_tw_reuse,必须设置tcp_timestamps=1,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让TCP连接出一些诡异的问题

    • 关于tcp_tw_reuse 官方文档上说tcp_tw_reuse加上tcp_timestamps可以保证协议角度上的安全,但是你需要tcp_timestamps在两端都被打开
    • 关于tcp_tw_recycle 如果tcp_tw_recycle打开的话,会假设对端开启了tcp_timestamps,然后回去比较时间戳,如果时间戳变大,就可以重用。但是,如果对端是一个NAT网络的话(如:一个公司只用一个IP出公网)或是对端的IP被另一台重用了,这个事就复杂了。建链接的SYN可能被直接丢掉(你可以看到connnection time out的错误)
    • 关于tcp_max_tw_buckets 这个是控制并发TIME_WAIT的数量,默认值是180000,如果超限,那么系统会把多的destory掉,然后在日志里打上警告(如:time wait bucket table overflow),官网文档说这个参数用来对抗DDOS攻击的。也说的180000默认值并不小。这个还要根据实际的情况考虑。

TIME_WAIT意味是你主动断开连接的,如果对方断连接,那么这个破问题就是对方的了。另外如果你的服务器是HTTP服务器,那么设置一个HTTP的KeepAlive相当重要(浏览器会重用一个TCP连接处理多个HTTP请求),让客户端去断链接。

数据传输中的 Sequence Number

参考自:TCP 的那些事儿

再理一遍OAuth2.0

OAuth在我印象中已经啃过几次了。大多数时候,好记性还是不如烂笔头,对自己的记忆力太过自信了,当时理解了,觉的妙,但是现在只记得一个妙字了,至于其它的已经忘的差不多了…

所以最好趁自己刚理解的那一刻,赶紧的尽量用自己组织的语言记录下来。

概念

首先,想到OAuth 2.0,脑海中先弹出三方交互的概念,三方即如下:

  • 资源提供者
  • 用户(资源拥有者)
  • 获取资源者

在来挨个解释下,资源提供者,好比新浪微博提供了发微博、获取微博列表,百度网盘提供了存储文件、访问文件列表的服务,其中新浪微博、百度网盘都是资源的提供者。用户就是资源拥有者,具体的某条微博、网盘里的某个照片都是具体用户的,没有得到用户允许,资源提供者不可供其他应用使用。获取资源者 就是一些非资源提供者的应用想要访问这些资源,比如一款冲印照片的应用需要访问你网盘里的照片。

资源提供者获取资源者用户(资源拥有者),为了引用方便,我们先将分别简称其为:B1B2C。站在B1的角度来说,它定义了OAuth服务,交互细节都有它来设计,整个流程都需要依赖它,其一般是拥有众多用户的高可用服务。对于B2来说,它“觊觎”B1庞大的用户群以及用户资源(为用户省心,避免再注册于在上传资源),它的最终目的是通过Access Token拿到想要的资源。对于C来说,它其实既是B1的用户,也是B2的用户,C在整个过程中只需要选中同意还是不同意。

搞清楚了这些概念后,如果你要做应用,最好确定下你是要做提供者的应用 还是获取者的应用,前者相当于是要在基于现有服务,整理好资源提供一整套OAuth 2.0的Server,后者作为一个OAuth 2.0的Client相对较为简单。

软件架构概念

有了以上的概念我们就能从宏观上来理解为什么会有OAht2.0的存在。如果要从软件上来实现它,基于以上三者,我们还需要细分一下,如下:

  • 资源提供者
    • HTTP service: HTTP服务提供商
    • Resource Server: 资源服务器,可能你需要整理归类下现有的资源,有的资源可能不需要列入资源服务器。比如新浪微博的接口分组
    • Authorization server: 认证服务,提供资源的授权服务
  • 用户(资源拥有者)
    • Resource Owner: 资源拥拥有者,用户
    • User Agent: 用户代理,这个就代表着用户,一般就是浏览器、操作界面。
  • 获取资源者
    • Third-part application: 第三方应用程序

应用

  • 作为第三方应用,避免注册环节,用微博、微信、QQ等登录/注册
  • 作为三方应用,访问用户的资源,比如分享微博,打印网盘照片等

运行流程

OAuth 2.0的运行流程如下:(referrence from RFC-6749)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+--------+                               +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---|
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+

步骤如下:

1
2
3
4
5
6
(A) 用户打开客户端后,客户端要求用户授权
(B) 用户同意给予客户端授权
(C) 客户端使用上一步获得的授权,向认证服务器申请令牌。
(D) 认证服务器对客户端进行认证后,确认无误,同意发放令牌
(E) 客户端使用令牌,向资源服务器申请获取资源
(F) 资源服务器确认令牌无误,同意向客户端开放资源

简单的来分析下,上面的流程是以请求为载体流转的,最终拿到Access token需要经过几个步骤,但是拿到的Access Token在失效前一直可以重复使用,也就是后面对资源的请求都是请求-应答这种方式。得到Access Token前花费两次“请求-响应”,一次是询问用户是否授权,另外一次是根据授权码请求得到Access Token。这一点与我们平时的普通web应用不同,平时web的认证只需要一次登录“请求-响应”,一般请求携带着用户名、密码,响应头里设置客户端sessionid至cookie。为什么前者需要两次,而后者只需要一次呢?因为后者服务端只需要验证用户名密码是否正确,而前者则需要转个弯,三方应用先向用户申请,申请同意过后再向资源提供的应用申请。

授权模式

前面讲到三方应用要向用户申请授权,这个过程要怎么做才合适呢?很显然这个授权的界面不能由第三来做,不然就没一点意义了,这个授权的界面都是资源提供方来做的,一般在界面还会提示一些文字让用户确认该界面是资源官方提供的,比如新浪微博:

这个提醒大多数普通用户是忽略的,因为现在也很少有黑客用这种方式用来盗取微博的密码了,不过这种方式在仿银行的网站上存在很多。

客户端必须得到用户的授权(authorization grant),才能获取令牌(Access Token),OAuth2.0定义了四种授权方式。

  • 授权码模式 (authorization code)
  • 简化模式 (implicit)
  • 密码模式 (resource owner password credentials)
  • 客户端模式 (client credentials)

授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点是通过客户端的后台服务器,与“服务提供商”的认证服务器进行互动。

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
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)

执行步骤:

1
2
3
4
5
1. 用户访问客户端,后者将其导向认证服务器 ----------------- 比如打开:https://passport.csdn.net/ ,点击“微博登录”,其链接导向的是:https://api.weibo.com/oauth2/authorize?client_id=2601122390&response_type=code&redirect_uri=https%3A%2F%2Fpassport.csdn.net%2Faccount%2Flogin%3Foauth_provider%3DSinaWeiboProvider
2. 用户选择是否给客户端授权
3. 假设用户选择了授权,认证服务器将用户导向至客户端事先指定的“重定向URI”(redirect URI),同时在URI后会附上一个授权码-----------------比如前面的新浪:点击授权,请求api.weibo/oauth2/autorize,响应“302”(重定向),响应头中的Location=https://passport.csdn.net/account/login?oauth_provider=SinaWeiboProvider&code=fbfd1c75c3309bb653a7c0816f919f49 ,即重定向地址。
4. 客户端根据“附带了授权码code的重定向URI”请求,即重定向的实现,客户端的后台服务器上向认证服务器申请令牌-token,这个请求-响应对于客户端与用户是不可见的
5. 认证服务器对授权码和重定向URI确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)

看下上面的一些请求细节。
第一步,客户端导向认证服务器的URI的参数:

  • clietn_id: 客户端ID,这个一般是三方应用向OAuth服务提供者申请得到的,签发后一般是固定不变的。
  • response_type: 授权类型,必选项,此处固定值为code。
  • redirect_uri: 表示重定向URI,可选项。
  • scope: 申请的权限范围,可选项。
  • state: 表示客户端当前状态,自定义之,认证服务器会原封不动的返回这个值。

第二步,授权界面:

  • 用户如果在资源提供方还未进行过认证(登录),那么需要用户先登录认证,会有用户名、密码等登录表单,登录通过后再是授权界面

第三步,授权与否:

  • 前面的步骤的假设是,用户选择了授权,界面上有时会有“取消”按钮,当用户拒绝授权时也可以通过URI重定向通知到客户端用户的行为

第四步,带授权码的URI重定向跳转,客户端的后台服务向认证服务器申请token

  • 授权码code,为了安全一般具有时效性,通常是10分钟,且使用一次后失效。该code与client_id是一一对应关系,即其他应用使用这个code是无效的,另外重定向的URI也是与之对应相对固定的。
  • 申请token的参数:(地址类似:POST //github.com/login/oauth/access_token)
    • grant_type:表示授权模式,必选项,此处为“authorization_code” (与之相呼应的是第一步请求里的 response_type=code)
    • code:表示上一步获得的授权码,必选项
    • redirect_uri:表示重定向URI,必选项,与第一步请求里的redirect_uri保持一致
    • client_id:表示客户端ID,必选项
    • client_secret:三方应用与OAuth服务商协商所得,注意保密,不要放置到客户端。(为什么不用上面的用户授权码code直接作为access token的原因其实也就在这里)

第五步,认证服务器检查token请求,响应参数如下:

  • access_token:表示访问令牌
  • token_type:表示令牌类型,可以是bearer或mac类型
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须在其他地方设置过期时间
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示授权范围,如果与客户端申请的范围一致,可省略。

其他三种授权模式

关于这三种模式,这里只会简单的记录下,如果需要详细了解,可以点文章末尾的链接去看阮一峰的文章。

  • 简化模式(implicit grant type)
    • 其特点是跳过了授权码这个步骤,所有步骤在浏览器中完成。 ——疑问
  • 密码模式(Resource Owner Password Credentials Grant)
    • 用户向客户端提供用户名、密码
    • 客户端使用用户提交的信息,提交给认证服务器申请令牌 通常这个客户端必须是高度可信任的,不然会有泄密的可能。
  • 客户端模式(Client Credentials Grant)
    • 客户端使用自己的名义,而不是用户的名义,向认证服务器认证。其实整个过程不存在授权,只有一个认证,就是服务器认证客户端是可信客户端。

实战

OAuth2集成——《跟我学Shiro》

关于安全

从以上的实现机制,可以看出最终是为了得到token。其实这个token与平时web会话客户端的那个 sessionid的本质意义是一样的,都是用户的身份标识。
为了sessionid的安全我们一般会做两点,后台设置的response cookie是httpOnly的,也就是js不可读取保证客户端安全,另外就是使用https保证传输通道的安全。按照这个思路我们可以类比下这个token该怎么在安全方面有所保障。

  • 尽量保证token 不能被三方代码访问
  • 请求token IP的白名单

安全问题,需要好好权衡,“三道安全门”你自己进门都会很困难,“一道安全门”足已,重要的是你要保护好自己的钥匙,插在门上不拔,再多安全门也没用。

总结

细节总难被记住,单个的web项目的身份认证只需要一个登录请求即可,而OAuth2.0涉及的是三方,会有两次请求,记忆的转弯点在这里,所以我的总结就是请求授权码,请求token


参考