一、定义
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
具体内涵这里就不做解释了,参考《什么是 JWT – JSON WEB TOKEN》。该文虽然回复里被喷,但能对JWT大致有个了解。
下面将利用PHP来解释什么是JWT,并且尝试保护其安全。
二、用途
不要被一堆名词搞得晕头转向,JWT只是一种token形式,可用来解决传统session的一些弊端,它本身和数据安全没有半毛钱关系。
1、session方法
传统session方法,一般过程是这样的:
服务端将部分数据存储于session中;该条数据的session ID
自动返回并存储于前端cookie(若cookie被禁用,PHP可手动获取并传递session ID
至前端);再次请求时自动将cookie中的session ID
传递给服务端(若cookie被禁用,PHP则手动传递session ID
至服务端);服务端根据前端传来的session ID
查询该条session数据,若存在则执行相关操作 。
这与通过ID查数据表一个意思,因此也可以利用数据库模拟session存储。
数据表中除了存储部分用户数据,也可以存入登录IP、有效时间等,在匹配时用以加强安全性。
这里举个“假定客户端被禁用cookie”的简易例子:
// 1、登录成功后服务端存储一些数据信息,并返回给客户端session IDsession_start();$_SESSION["userid"] = 13;$_SESSION["username"] = 'test';$_SESSION["contact"] = '13652044557';return session_id();// 2、客户端提交一个表单和sessionid,服务端接收$title = $_POST['title'];$content = $_POST['content'];// 使用session_id()定位url传递的Session ID:/?sid=bba5b2a240a77e5b44cfa01d49cf9669if(!empty(session_id($_GET['sid']))){session_start();$userid = $_SESSION["userid"];$username = $_SESSION["username"];$contact = $_SESSION["contact"];// 将title、content、userid等数据录入数据库// ...return TRUE;}return FALSE;
弊端就是每一个用户连接都会在服务器端中产生一堆数据和对应的session ID
,当用户很多时会导致服务器压力很大,另外当服务器有多台时还需考虑session ID
在这些服务器之间共享。
于是就有另一种解决方案:token。
2、token方法
服务端将部分数据及验证信息依据某种规则生成一个token
字符串;将token
发送给客户端,客户端将其存储于cookie或者Local Storage(本地存储)里;再次请求时将该token
传递给服务端。服务端将前端传来的token
拆解并验证,若验证成功则执行相关操作。大意就是将数据及验证信息混合后(名为token)存在客户端,使用时该token提交给服务器验证,如果成功则使用token中带的数据执行操作。
因为不需要将N个session存储在服务端,所以减少了系统开销;
因为客户端存储了验证信息,那么就实现了跨域验证。
弊端就是客户端存储字节变大,数据传递变多,本来只要一个sessionid,现在一个混合物~~
用网友的话说:session方法是空间换时间,token方法是时间换空间
如果还是不好理解token方法的话,这里再用个不怎么地的例子:
$key = '!@#~1314';// 1、登录成功后获取如下数据,并返回给客户端一个token$userid = 13;$username = 'test';$contact = '13652044557';$token = md5($key.$userid).'|'.$userid.'|'.$username.'|'.$contact;return $token;// 2、客户端提交了一个表单和token,服务端接收并验证$title = $_POST['title'];$content = $_POST['content'];$token = explode('|',$_POST['token']);//如果匹配密钥则证明userid等数据正确if($token[0] === md5($key.$token[1])){$userid = $token[1];$username = $token[2];$contact = $token[3];// 将title、content、userid等数据录入数据库// ...return TRUE;}return FALSE;
看了两个案例应该大致明白了吧?session方法是将信息存储在服务端,而token方法是将信息存储在客户端。
当然这是一个很粗糙的token方法,不可用于实践。
为了让token看起来高大上一点,咱们来个JWT。
3、JWT方法(token方法中的一种)
JWT由三个部分组成,每个部分用.
连接。
第一部分为头部(header)
头部承载两部分信息:声明类型
这里是jwt声明加密的算法
通常直接使用 HMAC SHA256
{'typ': 'JWT','alg': 'HS256'}
第二部分为载荷(payload, 类似于飞机上承载的物品)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:标准中注册的声明
iss
: jwt签发者
sub
: jwt所面向的用户
aud
: 接收jwt的一方
exp
: jwt的过期时间,这个过期时间必须要大于签发时间
nbf
: 定义在什么时间之前,该jwt都是不可用的
iat
: jwt的签发时间
jti
: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息。私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息。
{"sub": "1234567890","name": "test","admin": true}
第三部分为签证(signature)
jwt的第三部分是签证信息,这个签证信息由三部分组成: header (base64后的)payload (base64后的)secret(盐,全局唯一,绝对不能泄露,关键之关键
)
将header和payload两个部分使用base64url编码后联结起来,然后通过header部分指定的算法,生成第三部分签证。
$encodedString = base64_encode($header).'.'.base64_encode(urlencode($payload));$secret = 'secret'; //实践时请复杂化return hash_hmac('sha256', $encodedString, $secret);
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt,样式大概如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
客户端每次请求带着的JWT格式的token,服务端只要使用secret对比一下,就知道客户端有没有被篡改。
如果篡改过,那就直接拒绝;
如果没有被篡改过,那就取出JWT的过期时间,如果过期就响应过期内容,没有过期就取出payload做业务处理。
这样就省了session、redis存储唯一标识的问题。
但是,但是,但是看了案例后有没有一种心慌慌的感觉?“好家伙,这验证也太简陋了吧?”
session通过session ID来获取信息已经有点慌,这里仅凭一个secret验证后真伪就用上这些用户信息了?而且base64只是一种编码,并非加密,获得payload就能解码一部分信息了,总觉得不安全啊。
三、安全
那么如何更安全使用JWT呢?
三个部分header
、payload
、signature
规则方法在header
主要数据在payload
验证信息在signature
那么我们考虑安全的重点就在后两部分了。下面是个人的一些安全思路,可能存在很多问题,希望和大家一起讨论。
首先服务端的验证中至少带三个全局的salt
,salt1
用于保证第二部分,salt2
用于实现加密解密,salt3
用于生成JWT第三部分
$salt1 = '';$salt2 = '';$salt3 = '';
1、payload
建议内容
除了常用的用户名之类,建议必带下面的内容。
{"uid" : "c3f1dki2e1otc55v", //用户当前随机id[1]"iss": "admin",//该JWT的签发者"iat": 1573440582, //签发时间"exp": 1573940267, //过期时间,时间过后退出登录"nbf": 1573440582, //起始时间,该时间之前不接收处理该Token"domain": "",//限制域"ip": "127.0.0.1", //限制ip地址[2]"jti": "dff4214121e83057655e10bd9751d657" //Token唯一标识[3]}
[1] 除了每个用户唯一的32位id,还应有一个16位的随机id,每次登录时都会重置一个新的,这样外部获取的时候永远都是这个随机id,而不是真实id。[2] 限制ip、域、过期时间等等这些参数,都是用来验证该令牌归属和时效的,这里就不多说了。[3] 将uid、iss、iat等加salt1
后再md5,生成一个token存入jti,这样服务端获取到payload
中数据后,先将这些数据md5,看看是否符合这个token,就知道这些数据有没有被篡改了(第三部分的验证也是一样的道理,出于性能考虑可以省略本token)。
// 1、生成payload的token$payload = array("uid"=>"c3f1dki2e1otc55v","iss"=>"admin", "iat"=>"1573440582","exp"=>"1573940267","nbf"=>"1573440582","domain"=>"","ip"=>"127.0.0.1");// 生成第二部分的token$payload['jti'] = md5(json_encode($payload).$salt1);// 返回json格式return json_encode($payload);// 2、验证payload的token是否正确// json格式变数组$payload = json_decode($payload);// 获取token$jti = $payload['jti'];// 去掉token元素unset($payload["jti"]);// 验证是否正确if($jti = md5(json_encode($payload).$salt1)){return TRUE;}
2、加密与解密
base64_encode
和base64_decode
只是一种编码与解码[1],并非加密与解密[2],本文 “3、JWT方法(token方法中的一种)”中所举的例子也是这个方法。但实践时,如果被人截取了base64编码后的内容,很容易就解码出来所含内容了,这就带来了一定的安全隐患。
[1] 其实编码与解码的作用并不是考虑安全,更多是考虑压缩数据与防止字符在传递后出现乱码,尤其某些中文字符。所以这里推荐先可以先使用urlencode
一类的编码,去除乱码的风险,再到服务端去解码urldecode
。[2](这里用php举例)所以使用openssl_encrypt
来实现加密,因为有salt2
的存在,所以即便这里的数据被获取,只要没法获得salt,就没法解密。
下面的案例主要是表达一种思路,并未优化,比如其中urlencode
和urldecode
只要稍微优化一下,在第二部验证的时候就不需要了,节省开销。
注意:
hash_hmac()
遇到特殊符号等可能出错,注意使用反斜杠或urlencode
hash_hmac($algo, $msg, $key, $raw_opt)
参数$raw_opt
若为true
,则返回二进制乱码,经openssl_encrypt()
后其字符数相比使用false
后的字符数少请根据需要选择参数raw_output
是true/false
// 1、生成token//签名$signature = hash_hmac('sha256', urlencode($header.'.'.$payload), $salt3, true);$header = base64_encode($header); // 头部编码$payload = openssl_encrypt(urlencode($payload),'DES-ECB',$salt2); // 内容编码$signature= openssl_encrypt($signature,'DES-ECB',$salt2); // 签名编码$token = $header.'.'.$payload .'.'.$signature; //合成tokenreturn $token;// 2、拆解并验证token$arr = explode('.',$token);$header = base64_decode($arr[0]); // 解码$payload = urldecode(openssl_decrypt($arr[1],'DES-ECB',$salt2)); // 解密及解码$signature= openssl_decrypt($arr[2],'DES-ECB',$salt2); // 解密if($signature == hash_hmac('sha256', urlencode($header.'.'.$payload), $salt3,true)){return TRUE;}
除了openssl_decrypt
,还有很多自定义的加密解密方法,可以保证每次刷新都出现新的字符串。
3、ssl
ssl就不用多说了,最直接的加密传输,连获取JWT后解析这一步都给他屏蔽了。
参考:
什么是 JWT – JSON WEB TOKEN
攻击JWT的一些方法
从零入门HMAC-SHA256
讲真,别再使用JWT了!
Cookie和Session、SessionID的那些事儿
cookie、session、sessionId、token、登录
sessionId的生成过程和过期时间
Session攻击(会话劫持+固定)与防御
php中的session_id详解
PHP 会话(Session)实现用户登陆功能
php如何openssl_encrypt加密解密
PHP对称加密-AES
在PHP开发中六种加密的方法,你用的是哪种?
PHP在线加密平台简介
golang常用加密解密算法总结(AES、DES、RSA、Sha1MD5)
PHP hash_hmac sha256 遇到的坑 解决PHP与JAVA sha256结果不一致
PHP hash_hmac()用法及代码示例
DES和AES密码之间的区别 & 对称加密算法DES、3DES和AES 原理总结
byte 16进制 2进制理解