JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其
JWT的组成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
载荷(Payload)
{ “iss”: “Online JWT Builder”,
“iat”: 1416797419,
“exp”: 1448333419,
“aud”: “www.example.com”,
“sub”: "jrocket@example.com",
“GivenName”: “Johnny”,
“Surname”: “Rocket”,
“Email”: "jrocket@example.com",
“Role”: [ “Manager”, “Project Administrator” ]
}
iss: 该JWT的签发者,是否使用是可选的;
sub: 该JWT所面向的用户,是否使用是可选的;
aud: 接收该JWT的一方,是否使用是可选的;
exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
其他还有:
nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码
头部(Header)
JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{
“typ”: “JWT”,
“alg”: “HS256”
}
在头部指明了签名算法是HS256算法。
当然头部也要进行BASE64编码,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
签名(Signature)
将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在我们的请求URL中会带上这串JWT字符串:
下面我们从一个实例来看如何运用JWT机制实现认证:
登录
第一次认证:
- 第一次登录,用户从浏览器输入用户名/密码,提交后到服务器的登录处理的Action层(Login Action);
- Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);Login Action调用认证服务进行用户名密码认证,如果认证通过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);
- 返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;
- 生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;生成Token的过程中可以调用第三方的JWT Lib生成签名后的JWT数据;
- 完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登录过程;
请求认证
基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在COOKIE
中,也可能在HTTP的Authorization头中;
- 客户端(APP客户端或浏览器)通过GET或POST请求访问资源(页面或调用API);
- 认证服务作为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,如果没有找到,则在HTTP Authorization Head中查找;
- 如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;如果找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;
- 完成解码并验证签名通过后,对Token中的exp、nbf、aud等信息进行验证;
- 全部通过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
如果权限逻辑判断通过则通过Response对象返回;否则则返回HTTP 401;
对Token认证的五点认识
对Token认证机制有5点直接注意的地方:
- 一个Token就是一些信息的集合;
- 在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率
- 服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查
- 基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
- 因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;
以下是本人写的一个基于ci框架的demo
登陆方法
前端通过ajax请求login:参数用户名,密码
public function login()
{
$params = $this->input->post();
$userinfo = $this->db->where("username",trim($params['username']))->where("password",md5(trim($params['password'])))->get('user')->row();
if($userinfo){//验证用户合法生成token
$token = $this->get_token(array('uid'=>$userinfo->id,'username'=>$userinfo->username));
echo 1;//这里获取token
}else{
echo 0;//否则用户不存在
}
}
token获取方法可以封装到一个model里调用,这里测试直接写道action里
以下方式是将token设置到cookie里,这样浏览器下次请求就可以直接带着这个token了,就像以前你熟悉的sessionid一样,然后后台通过cookie取出token验证合法性,就是去比对你服务器存储的token,是否存在,是否过期等
private function get_token($data)
{
$this->load->helper('cookie');
$array = array(
"iat"=> time(), // issued At签发时间
"exp"=> time()+7200, // Expiration Time,过期时间
"nbf"=> time()+60, //该时间之前不接收处理该Token
"iss"=> "John Wu JWT", // Issuer,该JWT的签发者,个人理解随意定义
"aud"=> "localhost", // Audience,该JWT的接收者,个人理解服务器标识,可以写你的域名
"uid"=> $data['uid'],//以下是用户id等信息,不要放敏感信息和太多信息,一般放用户id,和用户名
"username"=> $data['username'],
);
$secret = 'localhost';
$header = base64_encode(json_encode(array('typ'=>'JWT','alg'=>'HS256')));
$payload = base64_encode(json_encode($array));
$token = $header.'.'.$payload.hash_hmac('sha256',$header.'.'.$payload,$secret);
set_cookie('x-access-token',$token,7200);//将生成的token设置到cookie里
}
经过上一步登陆就完成了,然后调用其它受登录保护的接口时就可以在后端设置拦截器验证用户身份,通过才返回数据,否则就是非法调用
codeigniter框架可以通过hooks钩子,对权限接口做一个拦截器,而不需要去改动框架的Controller.php文件,关于钩子的用法这里就不多说了,官方文档都有,需要注意的是,权限接口拦截器最好使用post_controller_constructor这个钩子是 在你的控制器实例化之后立即执行,控制器的任何方法都还尚未调用
另外,通过cookie传递token是模仿了session与cookie机制,而且是基于浏览器等支持cookie的客户端的,前端也可以通过ajax设置http的header来传输token,还有一种比较普通的方式就是直接通过post或get传递token,相信你最初接触token就是用的这种,比较麻烦,每个接口都要额外传token,如果你是app应用,那么你就不能使用cookie这种方式了