有关Web身份验证与JWT认证探究与实现

今天上班,群里有朋友问介绍一个好用的AUTH认证第三方类。被人戏称面向框架编成。想想自己,为了成为一个好的架构师,怎么说也要手动实现一下吧

1、JWT 简述

JWT全称(JSON Web Token)是一种用于web开发中,用于身份验证的一种机制。

这里也引用以下RFC7519的定义。

2、为什么使用JWT

我们来想象一下,普通开发流程中,出现用户身份认证,需要其登陆在能使用需求时,我们该如何实现呢?

实现这个需求首要条件就是判断来的这个请求到底是谁,但是http本身是无状态的,服务端无法判断这一次请求的人,是与上一次请求属于用一个端。而我们不可能在每次请求的时候都携带上用户的登陆账户与密码。其一是每次操作都需要验证是很没必要且浪费资源的,其二http安全性很低,用户信息容易被人恶意获取和使用。

此时引入了Session与Cookie的概念。当用户登陆后系统将用户登陆的信息存储在服务端(Session),抑或是客户端(Cookie)。

但这两种方式实现验证都不是太好,Cookies存在安全问题,是可以被伪造的。而Session则存在分布式集群同步问题,因为session只存在与用户登陆时认证的那台服务器里。如若时后期使用集群的话,那就还得考虑文件文件同步的问题。徒增没必要的io与资源消耗,还要使得架构变得无比复杂。

JWT就是为了解决这些难题而诞生的。它的工作流程就是用户登陆后,服务端根据用户登陆信息加上自己的密钥,生成一串TOKEN。用户拿到token后,在请求中携带这个token。服务端接受token判断这个token是否为我们颁发的。如果是,则验证成功。

3、JWT 功能实现

3.1 解构解析

以下就是jwt的构成要素,分别为Header,Payload,Sign

1
2
3
4
graph TB
Header
Payload
Sign

引用JWT官网中的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Header 中包含加密算法约定,与token类型约定
{
"alg":"HS265",
"typ":"JWT"
}

// Payload 中包含用户基础信息
// 这里可以填写用户公开信息,例如用户等级,角色信息,过期时间
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

// 而Sign 则为Header与Payload二者base64加密后,拼接在加密的字符串
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)

token是将三个参数由”.”拼接,格式为 Header.Payload.Sign。之此jwt成功生成

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

token颁发,用户拿到后可以通过base64解密拿到前两个公开信息。请求时携带token,服务端通过判断Header.Payload加密后是否等于Sign。如果等于则为受信任token。因为token对外暴露的只有加密方式,加密信息,加密密钥只有服务器持有。所以如果不知道密钥,是无法成功伪造能被服务器认证的token。

这样就算服务器做分布式集群,只要保证服务器token验证密钥一样,用户的每一个访问就都能被成功识别。另外还可以payload中的信息,对用户请求做权限管理。

3.2 功能实现

上面了解了jwt的验证方式与构造,我们就可以开始自己手动实现一个简单的JWT验证功能。默认加密算法为HS256

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
<?php
class JWT {
public $token = "";

private $header = [
"alg" => "HS256",
"typ" => "JWT"
];

private $payload = [];

private $data = [];

private $secret = "123456";

private $duration = 86400;

// 设置header
public function setHeader(array $header){
if(isset($header['alg']) || isset($header['typ'])){
return false;
}
$this->header = $header;
}
// 返回header
public function getHeader()
{
return $this->header;
}
// 设置额外参数
public function getData()
{
return $this->data;
}
// 设置获取额外参数
public function setData(array $data)
{
$this->data = $data;
}

private function getPayload()
{
$data = $this->data;
$time['iat'] = time();
$time['exp'] = $time['iat'] + $this->duration;

return array_merge($data,$time);
}
// 设置token生存时间
public function setDuration(int $time)
{
$this->duration = $time;
}
// 设置token生存时间
public function getDuration()
{
return $this->duration;
}
// 设置生成
public function genToken()
{
$header = $this->getHeader();
if (!isset($header['alg']) || !isset($header['typ'])) {
throw new Exception("please set header", 1);
}

$seg["header"] = $this->encrypt($this->getHeader());
$seg["payload"] = $this->encrypt($this->getPayload());
$seg["sign"] = $this->sign($seg["header"].".".$seg["payload"], $this->secret);

return implode(".", $seg);
}

/**
* 验证token
*
* @param string $token
* @return bool
*/
public function valify($token)
{
$seg = explode(".", $token);
list($header, $payload, $sign) = $seg;

$this->header = $this->decrypt($header);
$this->payload = $this->decrypt($payload);
// 过期
if ($this->payload['exp'] < time()) {
return false;
}

$vali = $this->sign("$header.$payload", $this->secret);
return hash_equals($sign, $vali);
}

public function sign($str, $key, $alg = "SHA256")
{
$token = hash_hmac($alg, $str, $key, true);
return base64_encode($token);
}

// 编码
public function encrypt(array $arr)
{
return base64_encode(json_encode($arr));
}
// 解码
public function decrypt(string $str)
{
return json_decode(base64_decode($str), true);
}
}

$jwt = new JWT();
$jwt->setData(["name" => "tom", "age" => 23]);

// 生成token
$token = $jwt->genToken();
echo $token.PHP_EOL;
// 输出:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJuYW1lIjoidG9tIiwiYWdlIjoyMywiaWF0IjoxNTU5NjM3NDI3LCJleHAiOjE1NTk3MjM4Mjd9.
// OTBmNTg5N2E0Y2JiM2JhMGIzNmQ4MmU5MGEyMTc3NjY5MzIzM2I3ZThmYjAxYjlmNDdhMzNlMjM0ZDc4ZDYzNw==

// 验证
var_dump($jwt->valify($token)); // bool(true)

此处代码为示例代码,有可能存在安全问题。请谨慎使用!

4、小结

web开发中JWT使用非常广泛,其传递方式有拼接在url上,也有放在http请求头中

1
2
3
GET http://example.com/api HTTP/1.1
Content-Type: applicatoin/json
Authrization: Bearar <token>

其内容扩展性也非常不错,比如可以在 payload 中加入用户等级,用户角色。这样后台可以在Middleware中就可以判断用户是否有权限访问该接口,达到系统解耦的效用。而且token是纯粹计算得出,不用以来其他有状态服务,如redis mysql等等。计算时没有IO操作,全在内存中完成,处理速度极快,非常适合高并发的场景。

当然,说了这么多有点,jwt也是有缺点的。即token一旦颁发,在有效期内绝对有效,无法收回。
所以为了安全考虑,架构师需要在系统架构师明确考虑好安全问题在使用。