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的一些弊端,它本身和数据安全没有半毛钱关系。
传统session方法,一般过程是这样的:
session ID
自动返回并存储于前端cookie(若cookie被禁用,PHP可手动获取并传递 session ID
至前端);session ID
传递给服务端(若cookie被禁用,PHP则手动传递 session ID
至服务端);session ID
查询该条session数据,若存在则执行相关操作 。这与通过ID查数据表一个意思,因此也可以利用数据库模拟session存储。
数据表中除了存储部分用户数据,也可以存入登录IP、有效时间等,在匹配时用以加强安全性。
这里举个“假定客户端被禁用cookie”的简易例子:
// 1、登录成功后服务端存储一些数据信息,并返回给客户端session ID
session_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:http://www.test.cn/?sid=bba5b2a240a77e5b44cfa01d49cf9669
if(!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。
token
字符串;token
发送给客户端,客户端将其存储于cookie或者Local Storage(本地存储)里;token
传递给服务端。token
拆解并验证,若验证成功则执行相关操作。大意就是将数据及验证信息混合后(名为token)存在客户端,使用时该token提交给服务器验证,如果成功则使用token中带的数据执行操作。
因为不需要将N个session存储在服务端,所以减少了系统开销;
因为客户端存储了验证信息,那么就实现了跨域验证。
弊端就是客户端存储字节变大,数据传递变多,本来只要一个sessionid,现在一个混合物~~
用网友的话说:session方法是空间换时间,token方法是时间换空间
如果还是不好理解token方法的话,这里再用个不怎么地的例子:
$key = 'thisismykey.com!@#~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。
JWT由三个部分组成,每个部分用 .
连接。
{
'typ': 'JWT',
'alg': 'HS256'
}
iss
: jwt签发者sub
: jwt所面向的用户aud
: 接收jwt的一方exp
: jwt的过期时间,这个过期时间必须要大于签发时间nbf
: 定义在什么时间之前,该jwt都是不可用的iat
: jwt的签发时间jti
: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击{
"sub": "1234567890",
"name": "test",
"admin": true
}
关键之关键
)将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 = 'thisissalt1.com';
$salt2 = 'thisissalt2.com';
$salt3 = 'thisissecret.com';
payload
建议内容除了常用的用户名之类,建议必带下面的内容。
{
"uid" : "c3f1dki2e1otc55v", //用户当前随机id[1]
"iss": "admin", //该JWT的签发者
"iat": 1573440582, //签发时间
"exp": 1573940267, //过期时间,时间过后退出登录
"nbf": 1573440582, //起始时间,该时间之前不接收处理该Token
"domain": "example.com", //限制域
"ip": "127.0.0.1", //限制ip地址[2]
"jti": "dff4214121e83057655e10bd9751d657" //Token唯一标识[3]
}
salt1
后再md5,生成一个token存入jti,这样服务端获取到payload
中数据后,先将这些数据md5,看看是否符合这个token,就知道这些数据有没有被篡改了(第三部分的验证也是一样的道理,出于性能考虑可以省略本token)。// 1、生成payload的token
$payload = array(
"uid"=>"c3f1dki2e1otc55v",
"iss"=>"admin",
"iat"=>"1573440582",
"exp"=>"1573940267",
"nbf"=>"1573440582",
"domain"=>"example.com",
"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;
}
base64_encode
和 base64_decode
只是一种编码与解码[1],并非加密与解密[2],本文 “3、JWT方法(token方法中的一种)”中所举的例子也是这个方法。但实践时,如果被人截取了base64编码后的内容,很容易就解码出来所含内容了,这就带来了一定的安全隐患。
urlencode
一类的编码,去除乱码的风险,再到服务端去解码 urldecode
。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; //合成token
return $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
,还有很多自定义的加密解密方法,可以保证每次刷新都出现新的字符串。
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进制理解