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闪亮登场!
什么是JWT?
JWT(Json Web Token)是一个开放标准(RFC 7519),它基于json对象定义了一种紧凑并且自包含的方式进行安全信息传输。由于消息经过了数字签名,所以是可以被校验和信任的。另外JWT可以使用密匙,或者使用RSA的公钥/私钥进行签名。
其中的一些概念:
- 紧凑:由于其较小的尺寸,JWT可通过URL,POST参数或HTTP标头内发送。 另外,较小的尺寸意味着传输速度很快。
- 自包含:JWT的数据中可以包含用户的必要信息,避免了多次查询数据库的情况。
为什么使用JWT?
session认证
因为http本身是无状态的协议,所以每一次的请求其实都要校验,session的原理就初次登陆的时候将相关信息保存到服务端,响应一个cookie保存到客户端,这样每次请求都携带cookie,服务器能够实现校验,这会面临3个问题
1、难以实现单点登录,除非不同服务器之间共享session
2、session默认保存在服务端,增加服务器的存储压力
3、API调试麻烦
OAuth 2.0
OAuth 一般用于第三方接入的场景,管理对外的权限,比如什么第三方登录,微信授权,开放平台等,类似这些更加严谨的场景,相对来说也更加安全,但是部署过程复杂,授权流程也是麻烦,感觉是有些小题大做。而JWT更适用于类似RESTful API(微服务)之间的交互。
自建token协议
这种情况当然最灵活,但是除非有雄厚的资金实例,多余的时间和必要的情况,否则没必要重复造轮子呐。
曾经我们还用过简单的办法,登陆之后根据用户信息进行加盐hash,该hash值即为token,然后以(hash,value)的形式存储在缓存或者数据库中,每次请求携带hash,然后读取校验该hash是否存在,否则校验失败。这种方式也不失为一种简单快捷的好办法,但是仅仅只能当做token校验,并且相关数据存储在服务器,每次访问都还需要进行一次查询,增加服务器开销
什么时候使用JWT?
下面是一些JWT有用的场景
1、身份校验
这是最常见的的使用场景,一旦用户完成了登陆校验,后面每一次的请求豆浆携带JWT,从而校验用户是否允许访问路由、服务、资源。更重要的是,通过JWT可以非常容易实现SSO(Single Sign On)单点登录,因为开销很小,这就意味着,在一个主站登陆了,别的站点就都可以轻松使用JWT访问。
2、信息交换
从上文可知,JWT是能够被签名的的,所以在安全信息传输中,是一个不错的方案,例如使用公钥私钥时,你可以确定收件人是谁,另外还可以校验确保内容是否被篡改。这样,就可以在一些类似下单、交易等等重要的场合使用。
JWT的基本结构
JWT由三部分组成,他们中间由.
分隔:
- Header 头部
- Payload 数据
- Signature 签名
因此,典型的JWT看起来是这样的
xxxxx.yyyyy.zzzzz
Header
头部主要包含2个部分,token类型和采用的加密算法。
{
"alg": "HS256",
"typ": "JWT"
}
然后用Base64Url进行编码,就成了JWT的第一个部分
Payload
数据部分包含了主要的声明字段以及相应的值,声明主要包括3种类型:reserved , public 和 private
- Reserved claims: 这些字段是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用。
常用的有:
iss(issuer): jwt签发者
sub(subject): 签发的项目
aud(audience): 接收jwt的一方
exp(exipre): jwt的过期时间,这个过期时间必须要大于签发时间
nbf(not before): 定义在什么时间之前,该jwt是不可用的.
iat(issued at): jwt的签发时间
jti(jwt 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 interceptor
axios.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 interceptor
axios.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_ROOT
axios.defaults.baseURL = baseUrl
// 初始化的时候加载本地储存过的jwt
if (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存储在本地的所有信息。
所以务必做好过滤安全检查。
总结
1、JWT并不包含权限校验部分,只包含Token校验,所以在Token校验完成之后,权限部分还需自行校验一次。
2、jwt的payload数据部分不要存放敏感信息,此部分是任何人都可以解密查看的,而jwt主要依靠签名校验身份,同时也不建议存放易改动的信息,否则需要token过期或者重新登录才能来获取最新的信息。
3、签名所用的secret私匙一定要保管好!!!
4、务必使用https,否则用户被截获到token,就可以进行伪造攻击。
5、JWT使用的场景中,一般是要跨域的,所以服务端需要做好CORS的策略支持。见这里
6、若需要强制过期JWT,则在用户表新建一个签名时间字段即可,在登陆的时候检查,若JWT保存签名时间小于服务器签名时间,即强制过期