太多地方有关于它的描述了,这里就不赘叙了,直接重点JWT(Json web token),主要应用场景是解决分布式站点的单点登录(SSO),同时传递一些非敏感性的数据。
官网:JSON Web Tokens - jwt.ioJSON 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 digitally signed using JSON Web Signature (JWS).https://jwt.io/
web应用大部分都是采用http/https无状态的协议进行传输数据,也就是说每次的请求都需要识别是哪个用户发出的请求,而session在用户输入账号密码后(非必须)就会在服务器生成基于浏览器用户登录的信息,并存储在服务端,然后响应回客户端存储在cookie中,客户端每次请求都会从cookie中读取session传递给服务器进行业务操作,具体流程如下:
session对于小站点来说是非常便捷的,但是应用中大型web应用就会有一定的弊端:
存储消耗: 每个客户端经过服务器响应之后,web应用都要在服务端保存一个session文件,以方便客户端下次请求的检测,通常而言session都是保存在内存中,随着访问用户的增多,服务端的开销就会明显增大。
分布式拓展: 由于当客户端首次访问服务器后都会生成session,并仅能存储在一个服务器上,当我们进行多台服务器做分布式时就会导致刚才登录完的用户又需要重新登录。当然这也有解决方案就是采用统一独立的session服务器,但是这个配置过程是复杂的,这就限制了分布式的拓展能力。同时还有分布式带来的单点登录问题,也是无法通过session来简单解决的。
CSRF: 因为是基于cookie来进行session识别的, cookie如果被截获(其实真的很容易),用户就会很容易受到跨站请求伪造的攻击。
http通过token识别也是无状态的,无需在服务器端保存用户的登录数据,也就是说不需要考虑用户在哪台服务器上登录了,只要客户端带上token,每个服务器都能识别出该用户的数据。
但是!每次的访问都需要服务器通过数据库对Token进行比对,无形之中就会增大数据库的压力。
至于怎么生成token那方式就五花八门了,常见的有md5、base64、uuid等。相比session而言有一定的进步,一是拓展,二是不一定需要cookie。
也大概展示下具体流程:
我们回到正题,先看下JWT最终成型的样子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJocmciLCJhdWQiOiJsb2NhbGhvc3QiLCJpYXQiOjE2NDc5MzMzMDAsIm5iZiI6MTY0NzkzMzMwMCwiZXhwIjoxNjQ3OTQwNTAwLCJqdGkiOiJiYTliZjA0YTQ5NGNlN2U1OTFhOThkZjlhY2Q2NzJkYyIsImFwcGtleSI6IkU2QTdFMkQ1RTAwQTY3QUVDMjY3QkQ5MzVFNzYzNkIyIn0.UND6YbUggeLGx3vLiB3uL2Fm-HKPjedRbvfdaeJRNF0
通过上面的字符串,我们能看到2个点将整个JWT分成了3部分 (header.payload.signature),这3部分分别对应是header(头部)、payload(有效载荷)、signature(签证),下面我们就开始详细讲解下这3个部分分别有什么作用,如何生成等。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
这是加密后的样子,主要是通过base64进行编码。
*(其实严格来说base64不是加密,这里说的加密是相对整体而言的。)
我们再来看下原始内容:
$header = array(
'alg' => 'HS256', //生成signature的算法
'typ' => 'JWT' //类型
);
JWT的header主要有两部分内容
①声明类型 typ :一般默认都是jwt
②声明加密方式 alg :一般默认是HMAC SHA256
// 通过json_encode把array转成json,然后通过base64_encode转成base64
$jwt_header = base64_encode(json_encode($header,JSON_UNESCAPED_UNICODE));
eyJpc3MiOiJocmciLCJhdWQiOiJsb2NhbGhvc3QiLCJpYXQiOjE2NDc5MzMzMDAsIm5iZiI6MTY0NzkzMzMwMCwiZXhwIjoxNjQ3OTQwNTAwLCJqdGkiOiJiYTliZjA0YTQ5NGNlN2U1OTFhOThkZjlhY2Q2NzJkYyIsImFwcGtleSI6IkU2QTdFMkQ1RTAwQTY3QUVDMjY3QkQ5MzVFNzYzNkIyIn0
有效载荷主要是存放有效信息,一般可以分为2种信息:标准信息、公共信息。
公共信息一般主要是用于存放用户相关或者业务需要的信息,但是不能存放敏感信息或者用户数据。
然后我们来看下上面有效载荷的原始内容:
$payload = [
//签发者 可以为空
"iss" => ( isset($param['iss'])&&!empty($param['iss']) ) ? $param['iss'] : "",
//面象的用户,可以为空
"aud" =>isset($param['aud'])?$param['aud']:"localhost;",
//签发时间
"iat" => ( isset($param['iat'])&&!empty($param['iat']) ) ? $param['iat'] : time(),
//在什么时候jwt开始生效
"nbf" => ( isset($param['nbf'])&&!empty($param['nbf']) ) ? $param['nbf'] : time(),
//token 过期时间
"exp" => ( isset($param['exp'])&&!empty($param['exp']) ) ? $param['exp'] : time()+5,
// 唯一标识
"jti" => ( isset($param['jti'])&&!empty($param['jti']) ) ? $param['jti'] : md5(uniqid('JWT').time()),
//签发者密钥-业务信息
"appkey" => ( isset($param['appkey'])&&!empty($param['appkey']) )?$param['appkey']:'',
];
上面的appkey内容就是业务所需要的信息。
$jwt_payload = base64_encode(json_encode($payload,JSON_UNESCAPED_UNICODE));
UND6YbUggeLGx3vLiB3uL2Fm-HKPjedRbvfdaeJRNF0
签证是需要用到前2步header和payload的base64之后的内容(非原始数据),所以要做签证之前需要把前2步处理好,最后还需要一个私有密钥secret,私有密钥的制定方式由自己控制,可简单的字符串,也可以md5后的内容。
*(私有密钥一定不能暴露在客户端,只能存放于服务器中,由于前2步都可以通过base64 decode进行解析,也就是相当于明文状态,一旦密钥泄露,别人就能随意伪造JWT,切记)
// 加密方式数组
$alg_config = array(
'HS256'=>'sha256'
);
// 将base64的header与base64的payload形成字符串
$input = $jwt_header . '.' . $jwt_payload;
// 进行签名加密
$signature = base64_encode(hash_hmac($alg_config[$alg], $input, $secret,true));
到此JWT所需要的header、payload、signature都已经准备好了,只需要将以上内容按照约定组合起来即可:JWT = header.payload.signature
一般客户端都会把JWT存放在头部信息中的Authorization中,并且习惯在JWT前面加bearer进行标注。
headers: {
'Authorization': 'Bearer ' + jwt
}
顺带说下jwt客户端存储的问题,web应用建议不要使用cookie,使用浏览器自带的本地存储storage。避免对cookie的依赖,由于部分用户会使用无痕或者禁用cookie导致jwt存储失败。
jwt.php
'HS256', //生成signature的算法
'typ'=>'JWT' //类型
);
//使用HMAC生成信息摘要时所使用的密钥
private static $key='';
/**
* 获取jwt token
* @param array $payload jwt载荷 格式如下非必须
* [
* 'iss'=>'jwt_admin', //该JWT的签发者
* 'iat'=>time(), //签发时间
* 'exp'=>time()+7200, //过期时间
* 'nbf'=>time()+60, //该时间之前不接收处理该Token
* 'sub'=>'www.admin.com', //面向的用户
* 'jti'=>md5(uniqid('JWT').time()) //该Token唯一标识
* ]
* @return bool|string
*/
public static function encode($param,$jwt_secret)
{
self::$key = $jwt_secret;
$payload = [
"iss" => ( isset($param['iss'])&&!empty($param['iss']) ) ? $param['iss'] : "", //签发者 可以为空
"aud" =>isset($param['aud'])?$param['aud']:"localhost;192.168.1.108;gzdx.tpddns.cn;liyun.gzdaoxun.com;", //面象的用户,可以为空
"iat" => ( isset($param['iat'])&&!empty($param['iat']) ) ? $param['iat'] : time(), //签发时间
"nbf" => ( isset($param['nbf'])&&!empty($param['nbf']) ) ? $param['nbf'] : time(), //在什么时候jwt开始生效
"exp" => ( isset($param['exp'])&&!empty($param['exp']) ) ? $param['exp'] : time()+3600*24, //token 过期时间
"jti" => ( isset($param['jti'])&&!empty($param['jti']) ) ? $param['jti'] : md5(uniqid('JWT').time()),
"appkey" => ( isset($param['appkey'])&&!empty($param['appkey']) )?$param['appkey']:'', // 签发者密钥
];
if(is_array($payload))
{
$base64header=self::base64UrlEncode(json_encode(self::$header,JSON_UNESCAPED_UNICODE));
$base64payload=self::base64UrlEncode(json_encode($payload,JSON_UNESCAPED_UNICODE));
$token=$base64header.'.'.$base64payload.'.'.self::signature($base64header.'.'.$base64payload,self::$key,self::$header['alg']);
return $token;
}else{
return false;
}
}
/**
* 验证token是否有效,默认验证exp,nbf,iat时间
* @param string $Token 需要验证的token
* @return bool|string
*/
public static function decode($Token,$jwt_secret,$debug=false)
{
self::$key = $jwt_secret;
$tokens = explode('.', $Token);
if (count($tokens) != 3){
if ($debug)
var_dump(1);
return false;
}
list($base64header, $base64payload, $sign) = $tokens;
//获取jwt算法
$base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
if (empty($base64decodeheader['alg'])){
if ($debug)
var_dump(2);
return false;
}
//签名验证
$de_sign = self::signature($base64header . '.' . $base64payload, self::$key, $base64decodeheader['alg']);
if ($de_sign != $sign){
if ($debug)
var_dump(3);
return false;
}
$payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);
//签发时间大于当前服务器时间验证失败
if (isset($payload['iat']) && $payload['iat'] > time())
{
if ($debug)
var_dump(4);
return false;
}
//过期时间小宇当前服务器时间验证失败
if (isset($payload['exp']) && $payload['exp'] < time())
{
if ($debug)
var_dump(5);
return false;
};
//该nbf时间之前不接收处理该Token
if (isset($payload['nbf']) && $payload['nbf'] > time())
{
if ($debug)
var_dump(6);
return false;
}
return $payload;
}
/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode编码实现
* @param string $input 需要编码的字符串
* @return string
*/
private static function base64UrlEncode($input)
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode解码实现
* @param string $input 需要解码的字符串
* @return bool|string
*/
private static function base64UrlDecode($input)
{
$remainder = strlen($input) % 4;
if ($remainder) {
$addlen = 4 - $remainder;
$input .= str_repeat('=', $addlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* HMACSHA256签名 https://jwt.io/ 中HMACSHA256签名实现
* @param string $input 为base64UrlEncode(header).".".base64UrlEncode(payload)
* @param string $key
* @param string $alg 算法方式
* @return mixed
*/
private static function signature($input, $key, $alg = 'HS256')
{
$alg_config=array(
'HS256'=>'sha256'
);
return self::base64UrlEncode(hash_hmac($alg_config[$alg], $input, $key,true));
}
}
先对上部分内容总结,下面还会加多一部分jwt实际应用之接口双重认证。
JWT优点:
其他内容:
全部JWT签名算法:
关于加密算法这块就不在这里赘述了,想了解全部内容的朋友可以按照这个表去逐一学习下。就在这里大概总结下对于单体应用来说HS256和RS256的安全性没多大区别,需要进行多方验证的微服务架构而言, RS256/ES256 安全性更高。
【前提】
在日常开发中我们会经常遇到多个系统需要进行消息互通的场景,一般消息互通都是采用API接口数据传输模式,这就需要我们对接口安全性负责。真实情况下会有各种各样的安全模式,但是这里就不讨论哪个最好,这里主要是将JWT更加深度应用起来(最简单的应用就是客户端+服务端的应用),JWT对于各个层级的开发者来说都相对比较容易理解和上手。
假设现在就有SCRM系统与ERP系统接口对接的业务。
【JWT应用介绍】
JWT采用双重认证模式,第一重认证访问接口用户有效性,并提取出appkey;第二重认证接口数据有效性。
appkey由header的Authorization中提取,无需通过接口post传输。
【加密算法】
第一重:Authorization由JWT模式生成,详情请看上文内容。
第二重:sign由MD5模式生成,具体公式:md5(appkey(jwt提取) + method(接口名) + timestamp(时间戳) + data(业务字段数组的json) + secret(接口密钥))生成,顺序可自由调整。
【data】
data主要是对象数组,里面函数接口需要的全部业务字段,先采用sort升序排序,再用Json Encode(保留中文)
原始data:
$data = [
"vip_num" => "v0001",
"vip_name" => "XX测试账号"
];
排序后data:
$data = [
"vip_name" => "XX测试账号",
"vip_num" => "v0001"
];
Json Encode后的data:
$data = {"vip_name" : "XX测试账号","vip_num" : "v0001"}
【secret】
appkey、jwt_secret、sign_secret的生成只要保证唯一性即可,业务上可以使用uuid或者结合业务内容组合,然后可以采用md5截取哈希值,再使用base64进行编码处理等操作。由于交互的双方都是服务器直接互通,所以令牌有一定安全性,只要服务器不被攻破,其次是双重认证,令牌存放不同位置,只丢失其中一个令牌不会对整体造成影响。