JWT(Json Web Token)初探与实践


协议标准: https://tools.ietf.org/html/rfc7519
jwt.io: https://jwt.io
开箱即用: https://jwt.io/#libraries

前言

最近网站后台迎来第三次改版,原来采用的是jquery+bootstrap这样常规的方式,可是随着网站的交互愈来愈多,信息量愈来愈大,就很是力不从心了,每次写动态交互都好痛苦。趁着此次机会,决定采用MVVM的新JS框架,最终评估选择vue.js大礼包,没错!正由于如此,先后端实现了彻底分离,就不能采用session这样简单的登录校验机制了,取而代之的是令牌+RESTful的方式进行交互,此时JWT闪亮登场!php

什么是JWT?

JWT(Json Web Token)是一个开放标准(RFC 7519),它基于json对象定义了一种紧凑而且自包含的方式进行安全信息传输。因为消息通过了数字签名,因此是能够被校验和信任的。另外JWT可使用密匙,或者使用RSA的公钥/私钥进行签名。
其中的一些概念:html

  • 紧凑:因为其较小的尺寸,JWT可经过URL,POST参数或HTTP标头内发送。 另外,较小的尺寸意味着传输速度很快。
  • 自包含:JWT的数据中能够包含用户的必要信息,避免了屡次查询数据库的状况。

为何使用JWT?

session认证
由于http自己是无状态的协议,因此每一次的请求其实都要校验,session的原理就初次登录的时候将相关信息保存到服务端,响应一个cookie保存到客户端,这样每次请求都携带cookie,服务器可以实现校验,这会面临3个问题
一、难以实现单点登陆,除非不一样服务器之间共享session
二、session默认保存在服务端,增长服务器的存储压力
三、API调试麻烦前端

OAuth 2.0
OAuth 通常用于第三方接入的场景,管理对外的权限,好比什么第三方登陆,微信受权,开放平台等,相似这些更加严谨的场景,相对来讲也更加安全,可是部署过程复杂,受权流程也是麻烦,感受是有些小题大作。而JWT更适用于相似RESTful API(微服务)之间的交互。vue

自建token协议
这种状况固然最灵活,可是除非有雄厚的资金实例,多余的时间和必要的状况,不然不必重复造轮子呐。
曾经咱们还用过简单的办法,登录以后根据用户信息进行加盐hash,该hash值即为token,而后以(hash,value)的形式存储在缓存或者数据库中,每次请求携带hash,而后读取校验该hash是否存在,不然校验失败。这种方式也不失为一种简单快捷的好办法,可是仅仅只能当作token校验,而且相关数据存储在服务器,每次访问都还须要进行一次查询,增长服务器开销react

何时使用JWT?

下面是一些JWT有用的场景
一、身份校验
这是最多见的的使用场景,一旦用户完成了登录校验,后面每一次的请求豆浆携带JWT,从而校验用户是否容许访问路由、服务、资源。更重要的是,经过JWT能够很是容易实现SSO(Single Sign On)单点登陆,由于开销很小,这就意味着,在一个主站登录了,别的站点就均可以轻松使用JWT访问。
二、信息交换
从上文可知,JWT是可以被签名的的,因此在安全信息传输中,是一个不错的方案,例如使用公钥私钥时,你能够肯定收件人是谁,另外还能够校验确保内容是否被篡改。这样,就能够在一些相似下单、交易等等重要的场合使用。jquery

JWT的基本结构


JWT由三部分组成,他们中间由.分隔:ios

  • Header 头部
  • Payload 数据
  • Signature 签名

所以,典型的JWT看起来是这样的
xxxxx.yyyyy.zzzzzgit

Header

头部主要包含2个部分,token类型和采用的加密算法。github

 
 
 
 
{ "alg": "HS256", "typ": "JWT"}

而后用Base64Url进行编码,就成了JWT的第一个部分算法

Payload

数据部分包含了主要的声明字段以及相应的值,声明主要包括3种类型:reserved , public 和 private

  • Reserved claims: 这些字段是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用。
    经常使用的有:
 
 
 
 
ississuer): jwt签发者subsubject): 签发的项目audaudience): 接收jwt的一方expexipre): jwt的过时时间,这个过时时间必需要大于签发时间nbfnot before): 定义在什么时间以前,该jwt是不可用的.iatissued at): jwt的签发时间jtijwt token id): jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击

须要注意的是,声明名称只有三个字符长度,这是为了让JWT保持紧凑

  • Public claims:公共的声明能够添加任何的信息,通常添加用户的相关信息或其余业务须要的必要信息.
  • Private claims:私有声明是提供者和消费者所共同定义的声明

简单示例以下:

 
 
 
 
{ "iss": "www", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "www@example.com", "from_user": "B", "target_user": "A"}

而后用Base64Url进行编码,就成了JWT的第二个部分

Signature

为了建立签名,你须要先对前面的部分进行Base64的编码,而后加上私匙,对其进行签名。
例如,你想使用HMAC SHA256算法进行前面,那么建立过程以下:

 
 
 
 
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名的目的是为了校验JWT的携带者信息,而且检验是否有篡改过所携带的JWT信息。
HMAC SHA256算法计算以后的二进制数据默认进行Base64编码,就是JWT的第三个部分了

将他们放在一块儿

最终的结果是三段Base64字符串,经过.拼接在一块儿,这样就很容易在HTML和HTTP环境中传输,与基于XML的标准相比,更加紧凑节省资源。

 
 
 
 
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

调试工具:https://jwt.io/#debugger-io

项目实践JWT

后端

项目使用的是基于php的thinkphp5.0框架做为后端提供服务。前端则是vue+element-ui+axios,至于php类库,采用的是php中Star最多的
https://github.com/lcobucci/jwt
后端php经过composer安装以后使用起来很是的简单,新建一个类专门用于校验

 
 
 
 
use Lcobucci\JWT\Builder;use Lcobucci\JWT\Parser;use Lcobucci\JWT\Signer\Hmac\Sha256;use Lcobucci\JWT\ValidationData;class Auth{ const KEY = 'febcbaae13751fa2ds44c2f107afb08d'; const VALID_INFO = [ 'Issuer' => 'http://www.xxxx.com', 'Audience' => 'http://aaa.xxxx.com', 'Subject' => 'test', 'Expire' => 259200 ]; public static function check() { $jwt = request()->header('jwt'); $valid = new ValidationData(); $valid->setIssuer(self::VALID_INFO['Issuer']); $valid->setAudience(self::VALID_INFO['Audience']); $valid->setSubject(self::VALID_INFO['Subject']); //校验jwt信息,同时校验签名,不然能够伪造信息 $signer = new Sha256(); if ($jwt->validate($valid) && $jwt->verify($signer, self::KEY)) { $uinfo = $jwt->getClaim('uinfo'); //取出数据的时候是对象而不是数组 $uinfo->id //后续的权限校验过程…… } } public static function getSignedJWT($userinfo) { $signer = new Sha256(); $token = (new Builder()) ->setIssuer(self::VALID_INFO['Issuer']) ->setAudience(self::VALID_INFO['Audience']) ->setSubject(self::VALID_INFO['Subject']) ->setIssuedAt(time()) ->setExpiration(time() + self::VALID_INFO['Expire']) //能够直接保存数组或对象 ->set('uinfo', $userinfo) ->sign($signer, self::KEY) ->getToken()->__toString(); return $token; }}

前端

登录的时候保存JWT到localStorage,退出登陆时前端删除保存的JWT便可。

 
 
 
 
apiLogin.login(this.$data.loginForm).then(res => { if (res.data.ret === 0) { this.$local.set('jwt', res.data.jwt) this.$local.set('menu', res.data.menu) this.$local.set('rules', res.data.rules) this.$local.set('username', this.loginForm.username) this.$local.set('title', res.data.title) this.$local.set('gpid', res.data.gpid) this.$router.push('index') // 本来没有jwt,因此登录获取以后手动设置一次 this.$http.defaults.headers.common['jwt'] = this.$local.get('jwt') } else { this.isLogining = false this.$message.error(res.data.msg) } }).catch(() => { this.isLogining = false })

base_api.js

 
 
 
 
import axios from 'axios'import { Message } from 'element-ui'import local from 'store'// Add a request interceptoraxios.interceptors.request.use(function (config) { return config}, function (error) { Message.error({ showClose: true, message: '网络异常,请检查您的网络' }) console.log(error) // Do something with request error return Promise.reject(error)})// Add a response interceptoraxios.interceptors.response.use(function (response) { // 受权过时,无受权信息,跳出登录 if (response.data.ret === 4011 || response.data.ret === 4013) { window.location.href = '/#/login' // 删除本地的token令牌 local.remove('jwt') Message.error({ showClose: true, message: response.data.msg }) return } if (response.data.ret === 4012) { // 无权限返回 window.history.back() Message.error({ showClose: true, message: response.data.msg }) return } return response}, function (error) { Message.error({ showClose: true, message: '网络异常,请检查您的网络' }) return Promise.reject(error)})const baseUrl = process.env.API_ROOTaxios.defaults.baseURL = baseUrl// 初始化的时候加载本地储存过的jwtif (local.get('jwt')) { axios.defaults.headers.common['jwt'] = local.get('jwt')}export const http = axios

关于安全性

Cookie 能够启用 HttpOnly 和 Secure:

  • HttpOnly:禁止浏览器的 JavaScript 环境访问 Cookie,防护针对 Cookie 的 XSS。
  • Secure:Cookie 只在 HTTPS 请求中被传输。

可是为了实现正真意义上的无状态和跨域单点,仍是坚持存储在LocalStorage,而目前localStorage存储没有对XSS攻击有任何抵御机制,一旦出现XSS漏洞,那么存储在localStorage里的数据就极易被获取到。
若是一个网站存在XSS漏洞,那么攻击者注入以下代码,就能够获取使用localStorage存储在本地的全部信息。
HTML5本地存储的安全性
因此务必作好过滤安全检查。

总结

一、JWT并不包含权限校验部分,只包含Token校验,因此在Token校验完成以后,权限部分还需自行校验一次。
二、jwt的payload数据部分不要存放敏感信息,此部分是任何人均可以解密查看的,而jwt主要依靠签名校验身份,同时也不建议存放易改动的信息,不然须要token过时或者从新登陆才能来获取最新的信息。
三、签名所用的secret私匙必定要保管好!!!
四、务必使用https,不然用户被截获到token,就能够进行伪造攻击。
五、JWT使用的场景中,通常是要跨域的,因此服务端须要作好CORS的策略支持。见这里
六、若须要强制过时JWT,则在用户表新建一个签名时间字段便可,在登录的时候检查,若JWT保存签名时间小于服务器签名时间,即强制过时

参考

1 2 3 4 5

相关文章
相关标签/搜索