JSON Web Token(JWT)初探

JSON Web Token 是一个开放标准( RFC 7519 ),它定义了一种紧凑且独立的方式,以 JSON 对象的形式在各方之间安全地传输信息。

JWT简介

不同的人对JWT也有着不同的看法,下面是我的理解

原理

简单来说,用户在经服务端认证成功后,返回一个带签名的json对象,此后用户的访问通信都由此json对象向服务端表明自己身份

特点

  • 服务器无需保存大量session数据,从有状态服务变为了无状态服务,这样有一个好处就是容易实现服务器的横向拓展,能够更好的实现负载均衡
  • 数据共享性强,因为服务器不保存JWT,每台服务器都能获取用户信息,利于实现跨域认证
  • 利于项目的前后端分离

JWT结构

JWT具体表现为一个 token 字符串,形如

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE

JWT由头部(header)、载荷(payload)与签名(signature)三部分组成,以“.”分割

前两部分可被 base64 解码为 json格式的字符串,签名则是通过指定算法对前两部分的加密所生成的信息,如

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

JWT其实是URL-safe的,因为其可能会被用于在URL中传递,为了避免URL解析错误,JWT的base64稍有些不同,具体实现可参考如下php代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  function base64UrlEncode($string) {
$data = base64_encode($string);
$data = str_replace(array('+','/','='),array('-','_',''),$data);
return $data;
}

public function base64UrlDecode($string){
$data = str_replace(array('-','_'),array('+','/'),$string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}

示例:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

alg 表示签名算法(algorithm),且默认是 HMAC SHA256,是一种使用单向散列函数来构造消息认证码的方法。

typ 表示类型,为与传统实现兼容,统一使用大写的JWT

Payload

payload是JWT的核心内容,存放一些要传递的数据,可以定义私有字段

官方字段示例:

1
2
3
4
5
6
7
8
9
{
"iss": "xxx"
"sub": "xxx",
"aud": "xxx",
"exp": 1551518526,
"nbf": 1551514827,
"iat": 1551514827,
"jti": "xxx"
}
  • “iss” (Issuer) Claim
  • “sub” (Subject) Claim
  • “aud” (Audience) Claim
  • “exp” (Expiration Time) Claim
  • “nbf” (Not Before) Claim
  • “iat” (Issued At) Claim
  • “jti” (JWT ID) Claim

一些敏感信息放在payload中可能会造成一些安全问题

Signature

signature 是对前两部分的签名,防止数据篡改

其使用的 secret key 存在服务端

1
2
3
4
5
6
7
8
public function GenToken(array $header,array $payload){
$jwt_header = $this->base64UrlEncode(json_encode($header));
$jwt_payload = $this->base64UrlEncode(json_encode($payload));
$jwt_hap = $jwt_header.".".$jwt_payload;
$signature = $this->base64UrlEncode(hash_hmac('sha256',$jwt_hap,$this->secret_key,true));
$jwt_token = $jwt_hap.".".$signature;
setcookie("token",$jwt_token);
}

JWT 安全

通过 jwt.io 的 Debugger 验证和生成JWT

使用“none”算法的JWT

部分JWT库在alg为none,signature为空时通过验证,以此可以构造任意payload欺骗服务器

漏洞示例:CVE-2018-1000531

更改 RS256 为 HS256

当服务器算法类型为 RS256 这种非对称加密算法时,如果修改算法类型为 HS256,服务器可能把原来的 public key 当作 secret key,此时我们就可以通过 HS256 算法用 public key 加密伪造的 payload 通过服务器验证

爆破 secret key

如果服务器的密钥较短的话可以使用爆破

爆破工具:

jwtbrute

jwtcrack

JWT cracker

使用JWT

服务端

https://jwt.io/ 中已经列出了很多JWT的签名/验证库以供使用

如 pyjwt、python-jose、jsonwebtoken

python-jose常用方法:

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
from jose import jwt,jws,jwk
from jose.utils import base64url_decode

token = jwt.encode({'key': 'value'}, 'secret', algorithm='HS256')
print token
print jwt.decode(token, 'secret', algorithms='HS256')

signed = jws.sign({'a': 'b'}, 'secret', algorithm='HS256')
print jws.verify(signed, 'secret', algorithms=['HS256'])

token = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"

hmac_key = {
"kty": "oct",
"kid": "018c0ae5-4d9b-471b-bfd6-eef314bc7037",
"use": "sig",
"alg": "HS256",
"k": "hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
}
key = jwk.construct(hmac_key)
message, encoded_sig = token.rsplit('.', 1)
decoded_sig = base64url_decode(str(encoded_sig))
print key.verify(message, decoded_sig)

output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSJ9.FG-8UppwHaFp1LgRYQQeS6EDQF7_6-bMFegNucHjmWg
{u'key': u'value'}
{"a":"b"}
True

python-jose 文档:

python-jose — python-jose 0.2.0 documentation

利用 jsonwebtoken 实现 RS256 签发/效验 JWT

生成私钥

ssh-keygen -t rsa -b 4096 -f jwtRS256.key

生成公钥

openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256Public.key

安装 jsonwebtoken

sudo npm install jsonwebtoken

rs256.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const jwt = require('jsonwebtoken')
const fs = require('fs')

const payload = {
name: 'guest',
admin: 'false'
}

const pri = fs.readFileSync('./jwtRS256.key')
const pub = fs.readFileSync('./jwtRS256Public.key')
const token = jwt.sign(payload,pri,{algorithm: 'RS256'})
console.log(token)

jwt.verify(token,pub,(error,decoded) => {
if (error) {
console.log(error.message)
return
}
console.log(decoded)
})

如果需求简单可以参考

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
<?php 

date_default_timezone_set('Asia/Shanghai');
error_reporting(0);

class JWT
{
public $secret_key = "your secret";

public function base64UrlEncode($string) {
$data = base64_encode($string);
$data = str_replace(array('+','/','='),array('-','_',''),$data);
return $data;
}

public function base64UrlDecode($string){
$data = str_replace(array('-','_'),array('+','/'),$string);
$mod4 = strlen($data) % 4;
if ($mod4) {
$data .= substr('====', $mod4);
}
return base64_decode($data);
}

public function GenToken(array $header,array $payload){
$jwt_header = $this->base64UrlEncode(json_encode($header));
$jwt_payload = $this->base64UrlEncode(json_encode($payload));
$jwt_hap = $jwt_header.".".$jwt_payload;

$signature = $this->base64UrlEncode(hash_hmac('sha256',$jwt_hap,$this->secret_key,true));
$jwt_token = $jwt_hap.".".$signature;
setcookie("token",$jwt_token);
}

public function VerToken($string){
$token_data = explode('.',$string);
$jwt_hap = $token_data[0].".".$token_data[1];
$signature = $this->base64UrlEncode(hash_hmac('sha256',$jwt_hap,$this->secret_key,true));
if ($signature === $token_data[2]){
return true;
}else{
return false;
}
}

public function decodeTokenAndGetjwtpart($string,$part){
$token_data = explode('.',$string);
if ($part === 'header')
{
return $this->base64UrlDecode($token_data[0]);
}
else if ($part === 'payload')
{
return $this->base64UrlDecode($token_data[1]);
}
else
{
return flase;
}
}

public function isExpiration($string)
{
$json_pay_data = $this->decodeTokenAndGetjwtpart($string,'payload');
$exp = json_decode($json_pay_data)->exp;
if ($exp)
{
if (time()>intval($exp))
{
return true;
}
}else
{
return true;
}

return false;
}

}

$jwt = new JWT();

?>

最好使用HTTPS协议加密传输内容

客户端

用户的状态在服务端的内存中是不存储的,所以这是一种无状态的认证机制,数据储存在客户端。

如果不跨域,可将JWT放在Cookie中自动发送,跨越请求时将JWT放在POST数据中

较好的方式是放在Authorization请求字段中

Authorization: Bearer <token>

References:

https://jwt.io/

RFC 7519 - JSON Web Token (JWT)

http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

文章作者: J0k3r
文章链接: http://j0k3r.top/2019/03/02/jwt-notes/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog