当前位置: 首页 > 后端技术 > Node.js

JSONWEBTOKEN(JWT)

时间:2023-04-03 17:13:54 Node.js

JWTisaformoftoken.Itmainlyconsistsofthreeparts:header(header),payload(load),andsignature(signature).Thesethreepartsareconnectedby".".AcompleteJWTvalueis${header}.${payload}.${signature},例如下面使用"."进行连接的字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTEiLCJpYXQiOjE2MTQzMjU5NzksImV4cCI6MTYxNDMyNTk4MH0.iMjzC_jN3iwSpIyawy3kNRNlL1mBSEiXtOJqhIZmsl8headerheader最开始是一个JSON对象,该JSON包含alg和typ这两个属性,对JSON使用base64url(使用base64转码后Theencodingalgorithmforprocessingspecialcharacterswillbeintroducedindetaillater)Thestringobtainedafterencodingisthevalueoftheheader.{"alg":"HS256","typ":"JWT"}alg:signaturealgorithmtype,whichneedstobeusedwhengeneratingthesignaturepartinJWT,defaultHS256typ:currenttokentypepayloadpayloadisthesameasheader,anditisalsoaJSONatthebeginningObject,thestringencodedwithbase64urlisthefinalvalue.Thereare7officiallydefinedattributesstoredinthepayload,andwecanwritesomeadditionalinformation,suchasuserinformation,etc.iss:issuersub:subjectaud:audienceexp:expirationtimenbf:effectivetimeiat:issuancetimejti:numbersignaturesignaturewillusethesignaturealgorithmdefinedbythealgattributeintheheadertoencryptthestringcombinedwiththeheaderandpayload,andtheencryptionprocessThepseudocodeisasfollows:HMACSHA256(`${base64UrlEncode(header)}.${base64UrlEncode(payload)}`,secret)Thestringobtainedafterencryptionisthesignature.base64urlbase64编码后,字符串中会有+、/、=三个特殊字符,JWT可能通过url查询传输,url查询不能包含+、/,urlsafebase64规定+和/替换为-和_分别,和=会造成url查询的歧义,所以=需要删掉,这就是整个编码过程,代码如下/***节点环境*@desc编码过程*@param{any}data待编码内容*@return{string}编码值*/functionbase64UrlEncode(data){conststr=JSON.stringify(data);constbase64Data=Buffer.from(str).toString('base64');//+->-///->_//=->constbase64UrlData=base64Data.replace(/\+/g,'-').replace(/\//g,'_').replace(/\=/g,'');returnbase64UrlData;}服务在解析JWT内容时,需要对base64url编码的内容进行解码。首先是将-和_转换为+和/。base64转码后得到的字符串长度可以被4整除,base64编码的内容最后只会有=。我们看下解码过程:/***节点环境*@desc解码过程*@param{any}base64UrlData待解码内容*@return{string}解码后内容*/functionbase64UrlDecode(base64UrlData){//-->+//_->///使用=补码constbase64LackData=base64UrlData.replace(/\-/g,'+').replace(/\_/g,'/');constnum=4-base64LackData.length%4;constbase64Data=`${base64LackData}${'===='.slice(0,num)}`conststr=Buffer.from(base64Data,'base64').toString();让数据;尝试{data=JSON.parse(str);}catch(err){数据=str;}returndata;}JWT使用node中的jsonwebtoken插件快速开发JWT。本插件主要提供sign和verify两个功能,分别用于JWT的生成和验证。下面是JWT生成和验证函数的简单实现:)}`,secret)*@param{json}payload*@param{string}secret*@param{json}options*/constcrypto=require('crypto');functionsign(payload,secret){constheader={alg:'HS256',//这里只是走下流程,直接用HS256签名typ:'JWT',};constbase64Header=base64UrlEncode(header);constbase64Payload=base64UrlEncode(有效负载);constwaitCryptoStr=`${base64Header}.${base64Payload}`;constsignature=crypto.createHmac('sha256',secret).update(waitCryptoStr).digest('hex');return`${base64Header}.${base64Payload}.${signature}`;}/***@descJWT验证*jwt内容是否被篡改*jwt老化验证,exp和nbf*@param{string}jwt*@param{string}secret*/constcrypto=require('crypto');functionverify(jwt,secret){//jwt内容是否被篡改const[base64Header,base64Payload,oldSinature]=jwt.split('.');constnewSinature=crypto.createHmac('sha256',secret).update(`${base64Header}.${base64Payload}`).digest('hex');如果(newSinature!==oldSinature)返回false;constnow=Date.now();const{nbf=now,exp=now+1}=base64UrlDecode(base64Payload);//jwt有效性校验,大于等于有效时间,小于过期时间returnnow>=nbf&&now{const{authorization}=req.headers;//1.验证传入的JWT是否可用constavailabel=verify(authorization,secret);if(!availabel){returnres.end();}//2.判断是否是当前JWT存在于黑名单中if(blacks.includes(authorization)){returnres.end();}//3.将当前JWT加入黑名单blacks.push(authorization);//4.生成一个newJWT并响应请求constnewJwt=sign({userId:'1'},secret);res.end(newJwt);}).listen(3000);每次请求刷新JWT会导致以下两个问题:问题1:每次请求都会把旧的JWT放入黑名单。随着时间的推移,黑名单越来越大,占用内存太多,每次查询时间太长。问题2:当客户端并行请求接口时,这些请求携带的JWT都是同一个值,请求总是按顺序进入服务,对于进入的请求,服务端会将当前的JWT放入黑名单第一的。输入请求后,服务端判断当前JWT在黑名单中后,会拒绝当前请求。问题一的解决方法:在JWT中定义了exp过期时间,程序设置定时任务,每次删除黑名单中过期的JWT。consthttp=require('http');constsecret='testsecret';//暂时用一个变量存储黑名单,实际生产中使用redis、mysql等数据库存储constblacks=[];functioncleanBlack(){setTimeout(()=>{blacks=blacks.filter(balck=>verify(balck));cleanBlack();},10*60*1000);//每10m清理一次黑名单}cleanBlack();http.createServer((req,res)=>{const{authorization}=req.headers;//1.验证传入的JWT是否可用constavailabel=verify(authorization,secret);if(!availabel){returnres.end();}//2.判断当前JWT是否在黑名单中if(blacks.includes(authorization)){returnres.end();}//3.将当前JWT放入黑名单blacks.push(authorization);//4.生成新的JWT并响应请求constnewJwt=sign({userId:'1',exp:Date.now()+10*60*1000,//10mexpires},secret);res.end(newJwt);}).listen(3000);问题2的解决方法:给黑名单中的JWT添加一个gracetime。如果当前请求携带的JWT已经在黑名单中,但是没有超过未给当前JWT的宽限时间,则后续代码会正常运行,如果超过则拒绝请求。consthttp=require('http');constsecret='testsecret';//暂时用一个变量直接存储黑名单,使用redis或者mysql存储constblacks=[];constgrace={};http.createServer((req,res)=>{const{authorization}=req.headers;constnow=Date.now();//1.验证传入的JWT是否可用constavailabel=verify(authorization,secret);if(!availablelabel){returnres.end();}//2.判断当前JWT是否在黑名单中,如果存在则判断当前JWT是否在graceperiod内constunavailable=blacks.includes(authorization)&&now>=(grace[authorization]||now);if(unavailable){returnres.end();}//3.当前JWT还没有加入黑名单时,将当前JWT放入黑名单if(!blacks.includes(authorization)){blacks.push(authorization);grace[authorization]=now+1*60*1000;//1mgracetime}//4.生成一个新的JWT并响应请求constnewJwt=sign({userId:'1'},secret);res.end(newJwt);}).listen(3000);注意:这个宽限时间是JWT加入黑名单时根据当前时间向后设置的时间节点。生成JWT时不添加。互斥登录使用JWT实现登录逻辑。为实现服务端的主动注销功能,服务端在下发JWT之前需要将JWT存储在用户和JWT对应的数据库中,等待服务端要主动注销用户,将用户对应的JWT加入黑名单。之后用户再次请求服务时,传入的JWT已经在黑名单中,请求将被拒绝。用户密码被修改,服务器主动取消用户登录功能,基本类似于互斥登录。