前段时间处理一个抽奖H5,测试过程当中想到若是有用户抓到抽奖接口,好比前端
https:xxx/lottery/userinfo
复制代码
若是直接访问抽奖接口,能够直接进行抽奖动做。这里就涉及处处理验证用户身份的问题ios
以后的解决方式是 判断接口的cookie中是否包含 userInfo 等参数信息git
不过还能够经过另一种方式来处理-- JWTgithub
JWT是通讯双方之间以 JSON对象的形式安全传递信息的方法。web
其实能够理解为使用非对称算法来进行先后端校验。redis
JWT 由三部分组成算法
typ 声明类型数据库
alg 声明加密的算法npm
而后按照此规则将头部信息进行base64编码,构成JWT第一部分json
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
复制代码
payload 就是存放有效信息的地方
payload 中有一些参数字段是建议使用的 (仅列出几个)
参数 | 含义 |
---|---|
iat | jwt的签发时间 |
exp | jwt的过时时间,这个过时时间必需要大于签发时间 |
nbf | 定义在什么时间以前,该jwt都是不可用的 |
好比来定义一个payload
{ "exp": Math.floor(Date.now() / 1000) + (60 * 60), "name": "John Doe" } 复制代码
payload 会进行base64编码,构成JWT第二部分
能够看到,签证部分是由三个部分组成的
参数 | 含义 |
---|---|
base64UrlEncode | base64加密后的Header |
base64UrlEncode | base64加密后的payload |
your-256-bit-secret | 自定义的加密secret |
secret 至关于私钥,不可泄漏,若是客户端能够拿到secret,就能够自我签发JWT了
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload) var signature = HMACSHA256(encodedString, 'secret') 复制代码
signature 是JWT的第三部分
将以上三部分拼接起来,就是最后的JWT
若是本身在生成jwt,有点复杂。目前已经有不少开发的第三方库来支持JWT。好比 jsonwebtoken
sign 用于生成 token
verify 用于检验token
koa-jwt 用于验证接口中是否包含token信息
搭建了一个简易的server 来看下效果
app.use( jwtKoa({secret: SECRET}) .unless({ path: [/\/login/] // 不须要经过jwt验证的请求路径 }) ) router.get('/login', async (ctx) => { let token = jwt.sign({ name: 'dva' }, SECRET) console.log(token, 'token') ctx.body = { token } }) router.get('/try', async (ctx) => { let token = ctx.header.authorization let result = jwt.verify(token, SECRET) ctx.body = { result } }) 复制代码
// {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZHZhIiwiaWF0IjoxNTMxMjgwMDg2fQ.Rh_vAKeytjAL2TbOk-MmXQWFesszjRU3Bzldrx5x17s"}% 复制代码
图片来自于文章 《先后端分离之JWT用户认证》
登陆拿到JWT
前端发起请求,Header中挂载JWT
因为我搭建的这个项目中有这种须要鉴权的接口比较少,因此并无使用koa-jwt来处理。只是用了 jsonwebtoken
构建两个接口 login , lottery
token = jwt.sign({ name: 'who', exp: Math.floor(Date.now() / 1000) + (60 * 60), // 设置 token 过时时间 }, SECRET) 复制代码
let token = this.headers.authorization // 解码 let decoded = jwt.verify(token, SECRET) // console.log(decoded, 'decoded') let {name} = decoded if (name != 'who') { code = 403 return } 复制代码
我这里拿到token以后将其写入了localStorage
window.localStorage.setItem('token', token) 复制代码
axios.get(`/${APP_NAME}/win`, { headers: { Authorization: token } }) 复制代码
若是传递错误的token 在server端JWT验证的时候就会报错
总而言之,若是你的接口须要考虑鉴权问题,能够参考下JWT来处理。
这篇文章发布以后,不少同窗提出了一些问题,这里一一回复。感谢各位的评论。
其实JWT在处理Header 和 payload 的时候,只是很简单的进行了Base64编码。 若是拿到某一个token的话,是很容易就将其解码出来的。
const base64url = require('base64url') let header = { 'typ': 'JWT', 'alg': 'HS256' } let resultH = base64url(JSON.stringify(header)) console.log(resultH, 'resultH') let payload = { name: 'dva', exp: 1531410000 } let result = base64url(JSON.stringify(payload)) console.log(result, 'result') // 解码payload let isP = 'eyJuYW1lIjoiZHZhIiwiZXhwIjoxNTMxNDEwMDAwfQ' let getP = base64url.decode(isP) console.log(getP, 'getP') 复制代码
因此不建议在payload中存放敏感信息,好比用户手机号,地址信息等
查阅资料后总结,jwt的续签更新目前有如下处理方式,基本的原理就是在某一个时间点,server端发放新的token
好比 在用户点击抽奖,发起请求的时候,server端每次更新一个token(我这里只更新jwt的有效期)
拿到新的token以后将其返回。下次发起抽奖动做的时候,挂载这个最新的token
可是这种处理方案有缺陷,若是用户两次请求的间隔时间超过了过时时间(好比20分钟),则接口过来的时候 首先会被判断为过时状态,请求终止(以后的代码不被执行,不会被下发新的token了)。用户会被强制退出到登陆界面。
能够说token比较重要的问题就是注销token。
好比我上面第二个jwt的项目,当用户点击退出登陆的时候,仅仅在客户端作了token的删除。
可是实际上这个token仍是处于有效期内的。若是用户保存了token值,在点击了退出登陆以后,实际还可使用此token值的。能够理解为伪注销。
传统的方式怎么处理用户的注销行为呢?-- 删除数据库记录。当用户注销登陆信息的时候,server会变动数据库信息
可是jwt是没有介入服务器来存储用户状态的。这就比较难处理了。咱们但愿token可以在用户注销后不能够被继续使用了
设置比较短的token 有效期,每次请求过来的时候,从新下发,不断更新token.
使用服务器存储token状态。当用户点击注销,将token置空。
能够理解为如何让一个token当即失效(有点像上面的问题3)
每一位用户在设备A登陆的时候,将UID和token对应关系存放起来
myRedis.set(`${uid}:token`, ${tokenA}) 复制代码
用户换设备B登陆的时候,将redis中的的token进行更新
myRedis.set(`${uid}:token`, ${tokenB}) 复制代码
每次请求发起的时候,server端去验证该UID对应的token信息是不是最新的token, 这样 若是携带的不是redis中的token的话,拒绝请求。前端强制退出登陆。
我将这个单点登陆的逻辑加入到了项目中。
你能够在两台设备使用同一个用户名进行登陆,尝试是否是能够将第一台设备的状态登出。
新增逻辑部分:将用户{name, token} 对应关系存放在文件中,每次发送抽奖请求的时候,判断文件最新的token与接口携带的token是否一致。不一致则反馈前端须要退出登陆
缺点:须要保留每一位用户的 {user, token} 对应关系
当用户点击退出登陆,此token则被放入黑名单(好比存放在redis)。若是有请求此时携带了黑名单中的token,则不予处理
缺点:久而久之黑名单数据量增加
重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程
好比用户的token被获取,那么即便用户登出了系统,其余人还能够利用Token模拟正常请求,而服务器端则没法判断这种状况。
仍是黑名单思路,每次token更新以后,或者用户登出以后,旧的token被放入黑名单。携带此token的请求一概不予处理
em,这个和上面的问题5有点矛盾,若是使用变化token的状况处理,那么确定会有当请求并发状态下,第一个请求在处理完毕拿到新的token,后面的请求携带的token就变成了旧的token,请求会失败
查阅资料后发现,有些人在将token存到黑名单的时候,会同时添加一个“宽限时间” 。当请求中携带了一个黑名单中的过时token,则去判断去“宽限时间”,若是在期宽限之间以内,则予以经过。
不过我我的没想明白,这种处理方式是否是有问题,既然已经被放入黑名单了,那为何又来一个“宽限时间”。为何不直接设置一个长一点的有效时间。
以上是对各位的一些回答。欢迎留言讨论。