JWT (音: jot ) 是目前最流行的跨域认证解决方案,全称JSON Web Token,基于JSON的开放标准((RFC 7519) ,以token的方式代替传统的Cookie-Session模式,用于服务器、客户端传递信息签名验证。
传统的登陆校验采用的是cookie-session方式,一般流程是这样的:
1、客户端使用用户名和密码请求登录。
2、服务器验证账号密码通过后,在当前对话(session)里面保存相关数据,比如用户UID、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到之前保存的数据,由此得知用户的身份,进而判断是否已经登陆等校验操作。
这种模式的缺点是扩展性不好。单机是没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。session一般是采用文件存储的方式,这带来文件同步以及读取的问题,即使将session放入redis中,也同样会带来高流量数据读取的问题。
JWT 采用的是Token令牌的方式,来进行校验,具体如下:
1、客户端使用用户名和密码请求登录。
2、服务器验证账号和密码通过后,服务端会签发一个 Token 返回给客户端。
3、客户端收到请求后会将 Token 缓存起来,比如放在浏览器 Cookie 中或者存储在Local Stage中,之后每次请求都会携带该 Token。
4、服务端收到请求后会验证请求中携带的 Token,验证通过则进行业务逻辑处理并成功返回数据
更直观的用图表示,如下图所示:
这样的一个流程的优点是:
1、服务端端不保存任何 session 数据了,也就是说,服务器变成无状态了,减小了服务器开销,从而比较容易实现扩展。
2、jwt构成简单,占用很少的字节,便于传输。
3、json格式通用,不同语言之间都可以使用。
观察上图中JWT字符串,可以发现,它由三部分构成,以点号.
字符分隔。分别是:
- 头部(header)
注意,JWT 内部是没有换行的,上图只是为了便于展示,将它写成了几行。
写成一行,就是下面的这个样子:
header.payload.signature
下面对3个部分分别介绍。
JWT的头部标识用于生成签名的算法,是一个 JSON 对象,包含了两个属性。长这个样子:
{
'typ': 'JWT',
'alg': 'HS256'
}
typ
属性表示这个令牌(token)的类型(type),默认为JWT
,一般不改,写死。
alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);
payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。它可以由三部分组成:
官方标准的字段申明
公共的字段申明
私有的字段申明
其中,JWT 规定了7个官方标准字段,不是必须填写的。JWT为了紧凑,声明名称都用三个字符的缩写:
iss: (issuer ) jwt签发者
sub: (subject) jwt主题
aud: (audience )接收jwt的一方
exp: (expiration time) jwt的过期时间,这个过期时间必须要大于签发时间
nbf: (not before))定义在什么时间之前,该jwt都是不可用的.
iat: (issued at) jwt的签发时间
jti:(JWT ID) jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
比如,一个playload的例子,如下:
{
"iss": "admin", //JWT签发者
"iat": 1535967430, //签发时间
"exp": 1535974630, //过期时间
"nbf": 1535967430, //该时间之前不接收处理该Token
"sub": "www.qq.com", //主题
"aud" => "ww.qq.com", //接收jwt的一方
"jti": "9f10e796726e332cec401c569969e13e", //该Token唯一标识
"name": "John Doe", //自定义的公共字段,姓名
"admin": true // 自定义的公共字段,管理员
}
signature 部分是对用密钥
对header
和 playload
两部分进行签名,防止数据被篡改。
具体操作如下:
key = 'secretkey'
unsignedToken = base64UrlEncode (header) + '.' + base64UrlEncode (payload)
signature = HMAC-SHA256(key, unsignedToken)
通过上面的步骤,我们依次得到了JWT的3个部分,接下来,就可以用点.
进行拼接,最终得到JWT完整部分:
JWT = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + base64UrlEncode(signature)
上面用到的base64UrlEncode编码,这个算法跟 Base64 算法基本类似,但是为了更好的在URL上进行传输,有这些不同:=
被省略、+
替换成-
,/
替换成_
。
具体一个php实现 base64UrlEncode 算法的例子:
/**
* base64UrlEncode
* @param string $input 需要编码的字符串
* @return string
*/
public function base64UrlEncode(string $input)
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
我们生成了JWT前面,一般有两种使用方式
1、客户端请求的时候在http头部携带 Authorization: bearer token,注意bearer后面有个空格
Authorization: Bearer
比如:
$ curl -XPOST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ" -H "Content-Type: application/json" http://127.0.0.1/user -d'{"username":"user1","password":"user1234"}'
2、加在url后面,通过一个get参数传递:
curl -XGET http://127.0.0.1/user?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjgwMTY5MjIsImlkIjowLCJuYmYiOjE1MjgwMTY5MjIsInVzZXJuYW1lIjoiYWRtaW4ifQ.LjxrK9DuAwAzUD8-9v43NzWBN7HXsSLfebw92DKd1JQ
服务器验证一个 jwt 的过程很简单,如下:
1、服务端收到 jwt。
2、将 header 和 payload 用密钥和对应的算法生成签名。
3、判断生成的签名和 jwt 第三部分是否一致。
4、不一致则返回错误,一致则表示 payload 内的数据可信,验证通过。
jwt逻辑很简单,也可以自己实现,也可以用公共开源的库,目前有几个库,很方便使用:
https://github.com/firebase/php-jwt
特点:非常简单,即刻上手。
下载使用:composer require firebase/php-jwt
https://github.com/lcobucci/jwt
特点:功能很全,使用链式函数增加属性,但是有点复杂。
下载使用:composer require lcobucci/jwt
目前用的gloang类库最多的是这个:
http://github.com/dgrijalva/jwt-go
特点:知名度高,广泛使用
下载使用:go get github.com/pascaldekloe/jwt
更多库和文档可以见官网: https://jwt.io/
jwt 的核心就是密钥,拥有密钥就拥有生成 jwt 的权利(千万不能泄露)。payload 中的数据不是加密的,不要放敏感数据。
参考资料:
https://jwt.io/
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html