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

手动实现一个上一篇写的jsonwebtoken

时间:2023-04-04 00:53:24 Node.js

,你会了解到jwt的实现原理和base64编码的原理。同时我也简单实现了jwt的生成,点这里。智威汤逊是什么?它本质上是经过签名的JSON格式数据。由于已签名,收件人可以验证其真实性。它也很小,因为它是JSON格式。JSONWebToken(JWT)是一种开放标准(RFC7519),它定义了一种紧凑且独立的方式,以JSON对象的形式在各??方之间传递信息。信息可以被验证和信任,因为它是数字签名的。JWT可以使用密钥(使用HMAC算法)或公钥和私钥(使用RSA算法)进行签名。为什么jwthttp协议需要是无状态的?长期以来,我们使用session/cookie来服务客户。服务器存储session,客户端存储一个sessionid。访问时,客户端携带sessionid,服务端使用对应的session。判断用户是否有相应的权限以及如何显示该页面;但目前随着终端设备的增多,更流行的开发模式是前后端分离,也就是说后端趋向于服务化并提供相应的操作接口,RESTfulAPI目前是一种一套比较成熟的接口规范;而RESTfulAPI提倡无状态,用今天所谓的jwt就可以实现无状态。session的stateful方式占用服务器内存较多,当项目较大时,可能需要使用rediscluster来存储session。使用jwt把用户的状态权限放在客户端,服务端可以根据传过来的token判断是否有权限访问这个资源。jwt的组成jwt由三部分组成,用.隔开:头部(header)载荷(payload)签名(Signature)一步步生成这个token,先用一个数组保存这三个部分,并声明一个数组constres=[];,得到三部分后,直接使用res.join('.')生成需要的token即可。header一般包含两部分,token的类型和使用的加密算法,如:constheader={alg:'HS256',typ:'JWT'};然后把这个序列化,转成base64编码,需要注意的是jwt中对应的base64并不是严格的base64,因为token可能会作为url,base64中的+/=三个字符会被转义,造成url变长,所以token的base64会把+转成-,/转成_,删除=。根据这个规则,我们来实现生成满足要求的base64的函数:constgetBase64UrlEscape=str=>(str.replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,''));constgetBase64Url=data=>getBase64UrlEscape(newBuffer(JSON.stringify(data)).toString('base64'));实现这个常用函数后,生成header就变得非常简单:res.push(getBase64Url(header));有效负载包含语句,它是用户和其他元数据的描述。包含三类声明:Reserveddeclarations,如exp,sub等。PublicdeclarationsPrivatedeclarations,privatedeclarations是自定义的payloads,个人倾向于在里面存放一些不相关的东西,比如用户名,用户权限,Userid,等:constpayload={username:'zp1996',id:1,authority:32};第二部分是将payload转换为base64编码:res.push(getBase64Url(payload));signatureheader和payload使用相应的密钥用相应的加密算法加密:sha256(`${base64UrlEncode(header)}.${base64UrlEncode(payload)}`,secret)一般支持的算法有:constalgorithmMap={HS256:'sha256',HS384:'sha384',HS512:'sha512',RS256:'RSA-SHA256'};consttypeMap={HS256:'hmac',HS384:'hmac',HS512:'hmac',RS256:'sign'};首先,我们了解一下几种加密算法:哈希加密——crypto.createHash()可以将任意长度的输入通过哈希转换为固定长度的输出,输出值为哈希值。如果两个哈希值不一样,那么原来的输入一定不一样;但是两个hash值相同,只能说原始输入很可能是相同的;因为哈希函数可能会发生“碰撞”。Hash非常快,问题是它非常快。虽然加密后无法逆向,但是可以使用彩虹表破解。由于哈希很快,我们可以根据彩虹表将其与待破解的加密字符进行对比,很快就会得到我们想要的结果。.当然一般都会进行加盐的操作,增加这个时间成本。回到今天说的jwt,jwt对安全性的要求还是很高的。jwt的前两部分是经过处理的base64,肯定是可以转义的。假设我们最后的签名部分已经被解密,那么token就可以任意伪造,应用就没有安全可言了。所以jwt在生成签名的时候并没有使用Hash加密。hmac加密——crypto.createHmac()我们先看下hmac的定义:key相关的哈希运算消息认证码。消息认证码可以用来保证消息不被他人伪造。消息认证码是一个带有密钥的散列函数。由于hmac有密钥,所以它会比hash有更好的安全性。签名加密——crypto.createSign()不仅对数据进行加解密,还需要保证数据传输的完整性和安全性。因此,需要采用的是Sign算法,主要采用非对称加密算法,使用私钥进行签名,使用公钥来验证数据的完整性。整个过程如下图所示:使用openssl生成私钥和公钥,我们看一个例子:constfs=require('fs'),crypto=require('crypto'),data='zp1996',alg='RSA-SHA256';constsigner=(method,key,input)=>crypto.createSign(method).update(input).sign(key,'base64');constverify=(method,pub,sign,input)=>crypto.createVerify(method).update(input).verify(pub,sign,'base64');constsign=signer(alg,fs.readFileSync('./private.pem'),数据);控制台.log(验证(alg,fs.readFileSync('./public.pem'),sign,data));//truejwtjwt中生成签名的方法主要使用hmac和sign,所以我们可以这样写生成Signature的方法:constcryptoMethod={hmac:(method,key,input)=>crypto.createHmac(method,key).update(input).digest('base64'),sign:(method,input)=>crypto.createSign(method).update(input).sign(key,'base64')};至此,我们可以编写一个完整的签名方法:constsign=(input,key,method,type)=>getBase64UrlEscape(cryptoMethod[type](method,key,input));/**payloadload*keykey*algorithm加密算法*type什么样的输入hmac或sign*/jwt.sign=(payload,key,algorithm='HS256',options={})=>{constsignMethod=algorithmMap[algorithm],signType=typeMap[algorithm],header={typ:'JWT',alg:算法},res=[];options&&options.header&&Object.assign(header,options.header);res.push(getBase64Url(header));res.push(getBase64Url(payload));res.push(sign(res.join('.'),key,signMethod,signType));返回res.join('.');};至此,一个比较合格的token就可以生成了,接下来会进行验证解析:对header和payload进行解析解析,说白了就是将base64转成普通的字符串,然后将字符串反序列化为得到难点在于token的base64已经处理过了。在替换的情况下转换回来很容易,用正则表达式也很容易做到,但是去掉的=怎么补呢?为什么要删除这个=?它有什么特别之处吗?我觉得有必要看看base64的原理:base64原理base64字符集base64是一种基于64个可打印字符表示二进制数据的方法,包括A-Z、a-z、0-9、+、/、补位用=,其实就是65个字符,看一张base64索引表:基本原理:将每3个8字节转换成4个6字节,然后转换后的4个6字节高位加2个0,形成48个字节,所以base64大约比原始字符串大1/3。看个例子,zpy怎么转成base64编码:首先确定ascii码,分别是122、112、121。由于只有两个字符,所以下一个字符对应的二进制值为00000000,所以组成的24位为:01111010|01110000|01111001分为六部分:011110|100111|,然后转换成十进制:30|39|1|57、结合上图中的索引表,很容易得到enB5的最终值。从这个规则我们可以很容易的得到zp是enAA,但是enA=是通过newBuffer('zp').toString('base64')得到的。仔细看base64,有两个规则:在两个字节的情况下:两个字节一共16个二进制位,按照上面的规则,转换成三组,最后一组除了前面加两个0,还要在后面加两个0。这样就得到了一个三位数的Base64编码,最后加一个“=”号。一个字节的情况:将这个字节的8个二进制位按照上面的规则分成两组,除了前面加两个0之外,最后一组后面加四个0。这样就得到了一个两位数的Base64编码,最后加上两个“=”号。既然解析tokenbase64的原理清楚了,那么如何给token加上=也很清楚了,无非就是在最后加一两个。base64编码长度必须是4的倍数,所以只需要在%4后面加=,可能的结果只有两种:2,加2=3,加1=所以可以写下面的代码来转换token转换为真正的base64:constgetBase64UrlUrlUnescape=str=>{str+=newArray(5-str.length%4).join('=');返回str.replace(/\-/g,'+').replace(/\_/g,'/');};到这里,解析方法就很容易出来了:constdecodeBase64Url=str=>JSON.parse(newBuffer(getBase64UrlUrlUrl(str),'base64').toString());jwt.decode=(token)=>{constsegments=token.split('.');return{header:decodeBase64Url(segments[0]),payload:decodeBase64Url(segments[1])};};验证之前基本就明白了,验证其实很简单,就是用key来比较验证是否正确:constverifyMethod={hmac:(input,key,method,signStr)=>signStr===sign(input,key,method,'hmac'),sign:(input,key,method,sign)=>{返回crypto.createVerify(method).update(input).verify(key,getBase64UrlUrlUnescape(sign),'base64');}};写在文末只是自己的浅见,如有错误请指出。