JSONWebToken是rfc7519发布的一个标准,使用JSON传输数据来判断用户是否登录。在jwt之前,使用session进行用户认证。下面的代码都是用javascript写的。见原文链接山月的博客session传统的登录方式是使用session+token。Token是指在客户端使用token作为用户状态凭证,浏览器一般存储在localStorage或者cookies中。Session是指在服务器端使用redis或者sql数据库来存储user_id和token的键值对关系。基本工作原理如下。constsessions={"ABCED1":10086,"CDEFA0":10010}//通过token获取user_id并完成认证流程函数getUserIdByToken(token){returnsessions[token]}如果是cookie中保存的就是session那是经常听到的+cookie的登录方案。其实存储在cookies、localStorage甚至IndexedDB或者WebSQL中,各有利弊,核心思想都是一样的。令牌身份验证与cookie中讨论了cookie和令牌的优点和缺点。如果不使用cookies,可以使用localStorage+Authorization进行认证。//httpheader,每次请求权限接口,都需要携带AuthorizationHeaderconstheaders={Authorization:`Bearer${localStorage.get('token')}`}推荐一个库localForage,使用IndexedDB、WebSQL和IndexedDB作为keys存储的值对。无状态登录会话需要在数据库中保存用户和令牌的相应信息,因此称为有状态。想象一下如何在不将用户状态保存在数据库中的情况下登录。第一种方式:前端直接将user_id传给服务端,缺点特别明显。作为任务user_id很容易被用户篡改,权限设置也没用。不过思路是对的,再往下走。改进:user_id的对称加密比上面稍微强一点。如果说前面的方法是空窗,这个方法就是贴纸窗。改进:user_id不需要加密,只需要签名保证不被篡改即可。JsonWebTokenjwt.iojwt由Header、Payload和Signature连接而成。HeaderHeader由非对称加密算法和类型组成,如下constheader={//加密算法alg:'HS256',type:'jwt'}PayloadPayload由RegisteredClaim和需要通信的数据组成。这些数据字段也称为声明。RegisteredClaim中比较重要的是“exp”Claim表示过期时间,过期时间会在用户登录时设置。constpayload={//表示jwt创建时间iat:1532135735,//表示jwt过期时间exp:1532136735,//通信用户iduser_id:10086}SignatureSign由Header、Payload和secretOrPrivateKey计算得出。对于secretOrPrivateKey,如果加密算法使用HMAC,则为字符串,如果使用RSA或ECDSA,则为PrivateKey。//通过HMACSHA256算法签名,secret不能泄露constsign=HMACSHA256(base64.encode(header)+'.'+base64.encode(payload),secret)//jwt由三部分组成constjwt=base64.encode(header)+'.'+base64.encode(payload)+'.'+sign从生成的jwt规则中,我们知道客户端可以解析出payload,所以payload中不要携带敏感数据,比如生成的用户密码验证,从规则中可以看出前两部分jwt的是header和payload的base64编码。服务端收到客户端的token后,解析前两部分得到header和payload,通过header中的算法用secretOrPrivateKey进行签名,判断是否与jwt中的签名一致。如何判断token过期?应用从上面可以看出,jwt并没有对数据进行加密,而是对数据进行签名,保证数据不会被篡改。除了用于登录,还可以用于邮箱验证和图形验证码。图形验证码登录时,如果密码输入错误次数过多,会出现图形验证码。图形验证码的原理是给客户端一个图形,将与图片配对的字符串保存在服务器端,过去多通过session实现。与验证码配对的字符串可以作为无状态验证的秘密。constjwt=require('jsonwebtoken')//假设验证码为字符验证码,字符为ACDE,10分钟后过期consttoken=jwt.sign({userId:10085},secrect+'ACDE',{expiresIn:60*10})邮箱验证现在网站会在注册成功后进行邮箱验证。具体方法是发送一个链接到邮箱,用户点击链接验证成功。//绑定邮箱和用户idconstcode=jwt.sign({email,userId},secret,{expiresIn:60*30})//在此链接验证验证码constlink=`https://example.com/code=${code}`StatelessVSStateful关于stateless和stateful,也有其他技术方向的对比,比如React的stateLess组件和stateful组件。函数式编程中的sideeffects可以理解为state,http也是一个无状态的协议,需要依赖headers和cookie来承载state。在用户认证中,presenceorabsence指的是是否依赖外部数据存储,比如mysql、redis等。想想下面关于登录的问题。如何使用session和jwt实现用户注销时,如何让token失效。因为jwt是无状态的,不保存用户设备信息,不可能简单的用它来完成上面的问题。您可以使用数据库来保存一些完成状态。session:清除user_id对应的token即可jwt:使用redis维护一个黑名单,在用户注销时加入黑名单(签名),过期时间与jwt过期时间一致。如何让用户只在一台设备上登录,比如微信session:使用sql数据库,在用户数据库表中添加token字段并添加索引,每次登录时重新设置token字段,根据找到user_idjwttokenpermissionspermissionpermissioninterface:如果使用sql数据库,在用户数据库表中添加一个token字段(不用加索引),每次登录重新设置token字段,获取user_id每次请求一个权限接口根据jwt,根据user_id查user表得到token判断token是否一致。另外,也可以使用counter的方法,如下一题。对于这个需求,session稍微简单一些,毕竟jwt也需要依赖数据库。如何让用户只在最后五个设备登录,比如很多玩家会话:使用sql数据库创建一个token数据库表,包含三个字段:id、token、user_id。用户表和令牌表具有1:m的关系。为每个登录添加一行。根据token获取user_id,再根据user_id获取用户登录了多少台设备。如果设备超过5个,则删除id最小的行。jwt:使用计数器,使用sql数据库,在user表中添加一个字段count,默认值为0,每次登录count字段自增1,在创建的jwtpayload中携带数据current_count通过每次登录作为用户的计数值。每次请求权限接口,根据jwt获取count和current_count,根据user_id查user表获取count,判断current_count与current_count的差值是否小于5。对于这个需求,jwt稍微简单一些,session需要额外维护一张token表。如何让用户只在最后五个设备上登录,让一个用户踢掉除现有设备以外的所有其他设备,比如很多玩家会话:基于上一个问题,删除除此设备以外的所有其他设备的token记录。jwt:在上一题的基础上,count+5,重新给device赋值一个新的count。如何显示用户登录设备列表/如何踢特定用户会话:在token表中新增一列devicejwt:服务端需要保留设备列表信息,方法同session,使用jwt没有意义。总结从以上问题来看,如果不需要控制登录设备的数量和设备信息,statelessjwt是一个不错的选择。一旦涉及到设备信息,就需要在jwt中加入额外的状态支持,增加了鉴权的复杂度。这时候session是一个不错的选择。Jwt不是万能的。是否使用jwt需要根据业务需求来决定。
