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