摘要:利用消息認(rèn)證碼可以確保消息不是被別人偽造的,消息認(rèn)證碼是帶密鑰的函數(shù),由于有了一個(gè),所以會(huì)比有更好的安全性。所以需要采用的就是算法,該算法主要利用的是不對稱加密算法,利用私鑰進(jìn)行簽名,公鑰驗(yàn)證數(shù)據(jù)的完整性。
寫在前面
本文會(huì)到你了解jwt的實(shí)現(xiàn)原理,以及base64編碼的原理。同時(shí)本人也簡單的實(shí)現(xiàn)了一下jwt的生成,點(diǎn)這里。
jwt是什么為什么需要jwt本質(zhì)上它是一段簽名的 JSON 格式的數(shù)據(jù)。由于它是帶有簽名的,因此接收者便可以驗(yàn)證它的真實(shí)性。同時(shí)由于它是 JSON 格式的因此它的體積也很小。
JSON Web Token (JWT)是一種開放標(biāo)準(zhǔn)(RFC 7519),其中定義了一種緊湊 (compact) 且自包含(self-contained)的方式用于以JSON對象的形式在多方之間傳遞信息。信息可以被核實(shí)和信任,因?yàn)樗?jīng)過了數(shù)字簽名。JWT既可以使用密鑰(采用HMAC算法),也可以使用公私鑰(采用RSA算法)進(jìn)行簽名。
http的協(xié)議是無狀態(tài)的,所有很長一段時(shí)間內(nèi),我們利用session/cookie來客服,服務(wù)器端存儲(chǔ)session,客戶端存儲(chǔ)一個(gè)sessionid,訪問時(shí)客戶端帶著sessionid,服務(wù)器根據(jù)對應(yīng)的session,來確定是否該用戶是否有相應(yīng)的權(quán)限以及如何展現(xiàn)這個(gè)頁面;但是目前隨著終端設(shè)備的增多,比較流行的開發(fā)模式為前后端分離,也就是說后端趨向于服務(wù)化,提供相應(yīng)操作的接口,RESTful API是目前比較成熟的一套接口規(guī)范;而RESTful API倡導(dǎo)的就是無狀態(tài),而無狀態(tài)可以利用今天所說的jwt來實(shí)現(xiàn)。session這種有狀態(tài)的方式,要大量的占用服務(wù)器的內(nèi)存,同時(shí)當(dāng)項(xiàng)目很大時(shí),可能需要借助于redis集群來存儲(chǔ)session。利用jwt可以將用戶狀態(tài)權(quán)限等放到客戶端,服務(wù)端根據(jù)傳過去的token來判斷是否有訪問這個(gè)資源的權(quán)限。
jwt的組成jwt包含了三部分,用.進(jìn)行分隔:
頭部(header)
載荷(payload)
簽名(Signature)
下面來一步步的生成這個(gè)token,先利用一個(gè)數(shù)組來保存這三個(gè)部門,聲明一個(gè)數(shù)組const res = [];,得到三個(gè)部分后,利用res.join(".")來生成所需的token就好。
頭部頭部中很一般包含兩部分,token的類型與采用的加密算法,如:
const header = { alg: "HS256", typ: "JWT" };
而后將,這個(gè)進(jìn)行序列化并且轉(zhuǎn)化為base64編碼,需要注意的是jwt中對應(yīng)的base64并不是一個(gè)嚴(yán)格意義上的base64,由于token有可能被做為url,而base64中的+/=三個(gè)字符會(huì)被轉(zhuǎn)義,導(dǎo)致url變得更長,所以token的base64會(huì)將+轉(zhuǎn)化為-、/轉(zhuǎn)化為_、刪除=。
根據(jù)這個(gè)規(guī)則,來實(shí)現(xiàn)一下生成符合要求的base64的函數(shù):
const getBase64UrlEscape = str => ( str.replace(/+/g, "-") .replace(///g, "_") .replace(/=/g, "") ); const getBase64Url = data => getBase64UrlEscape( new Buffer(JSON.stringify(data)).toString("base64") );
實(shí)現(xiàn)了這個(gè)公共函數(shù)后,生成header就變得很簡單了:
res.push(getBase64Url(header));載荷
載荷中包含了聲明,聲明是對于用戶的敘述以及其他的元數(shù)據(jù)。含有三種類型的聲明:
保留聲明,如exp,sub等
公有聲明
私有聲明,私有聲明為自定義的
對于載荷來說,個(gè)人傾向于在里面存儲(chǔ)一些無關(guān)緊要的東西,如用戶名,用戶權(quán)限,用戶id等:
const payload = { username: "zp1996", id: 1, authority: 32 };
第二部分就是將payload轉(zhuǎn)化為base64編碼:
res.push(getBase64Url(payload));簽名
簽名就是將編碼后的頭部、載荷,利用相應(yīng)的密鑰應(yīng)用相應(yīng)的加密算法進(jìn)行加密:
sha256( `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`, secret )
支持的算法一般有:
const algorithmMap = { HS256: "sha256", HS384: "sha384", HS512: "sha512", RS256: "RSA-SHA256" }; const typeMap = { HS256: "hmac", HS384: "hmac", HS512: "hmac", RS256: "sign" };
首先先來了解一下關(guān)于加密的幾種算法:
Hash加密—crypto.createHash()
通過散列可以把任意長度的輸入轉(zhuǎn)化為固定長度的輸出,輸出的值就為散列值。如果兩個(gè)散列值是不相同的,那么原始輸入一定不相同;但是兩個(gè)散列值是相同的,只能說原始輸入有很大的可能是相同的;因?yàn)樯⒘泻瘮?shù)有可能會(huì)發(fā)生“碰撞”。hash很快,問題也在于很快。雖說加密之后不可以逆推,但是可以通過彩虹表的方式來破解,由于hash是很快的,根據(jù)彩虹表來與待破解的加密字符進(jìn)行比對,很快就會(huì)得出我們想要的結(jié)果。當(dāng)然一般的會(huì)進(jìn)行加鹽的操作,來增加這個(gè)時(shí)間成本。回到今天說的jwt,jwt對于安全的要求還是很高的,jwt的前兩個(gè)部分就是一個(gè)經(jīng)過加工的base64,肯定可以轉(zhuǎn)義出來的,假設(shè)我們最后一個(gè)的簽名部分都被解密出來了,那么token就可以被任意偽造,應(yīng)用也就沒有了安全性可言。所以jwt在生成簽名時(shí)并沒有采取Hash加密。
Hmac加密—crypto.createHmac()
先來看一下hmac的高大上的定義:密鑰相關(guān)的哈希運(yùn)算消息認(rèn)證碼。利用消息認(rèn)證碼可以確保消息不是被別人偽造的,消息認(rèn)證碼是帶密鑰的hash函數(shù),由于hmac有了一個(gè)key,所以會(huì)比hash有更好的安全性。
Sign加密—crypto.createSign()
除了對數(shù)據(jù)進(jìn)行加密和解密外,還需要對于數(shù)據(jù)傳輸過程中的完整性,安全性是否得到了保證。所以需要采用的就是Sign算法,該算法主要利用的是不對稱加密算法,利用私鑰進(jìn)行簽名,公鑰驗(yàn)證數(shù)據(jù)的完整性。整個(gè)過程可以參見下圖:
私鑰和公鑰利用openssl來生成,下面來看一個(gè)例子:
const fs = require("fs"), crypto = require("crypto"), data = "zp1996", alg = "RSA-SHA256"; const signer = (method, key, input) => crypto.createSign(method) .update(input) .sign(key, "base64"); const verify = (method, pub, sign, input) => crypto.createVerify(method) .update(input) .verify(pub, sign, "base64"); const sign = signer(alg, fs.readFileSync("./private.pem"), data); console.log(verify( alg, fs.readFileSync("./public.pem"), sign, data )); // true
jwt中主要利用了hmac與sign,于是我們就可以寫出生成簽名的方法:
const cryptoMethod = { hmac: (method, key, input) => crypto.createHmac(method, key).update(input).digest("base64"), sign: (method, input) => crypto.createSign(method).update(input).sign(key, "base64") };
至此,我們就可以寫出一個(gè)完整的sign方法了:
const sign = (input, key, method, type) => getBase64UrlEscape( cryptoMethod[type](method, key, input) ); /* * payload 載荷 * key 密鑰 * algorithm 加密算法 * type 采用何種類型 hmac or sign */ jwt.sign = (payload, key, algorithm = "HS256", options = {}) => { const signMethod = algorithmMap[algorithm], signType = typeMap[algorithm], header = { typ: "JWT", alg: algorithm }, 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)); return res.join("."); };
至此就可生成一個(gè)比較合格的token了,下面就來實(shí)現(xiàn)驗(yàn)證與解析:
解析解析出header和payload,這件事說白了其實(shí)就是將base64轉(zhuǎn)化為普通字符串,而后再將字符串反序列化就可以得出。難點(diǎn)在與token的base64是被加工過得,對于替換的情況很容易轉(zhuǎn)化回去,利用正則很容易做到,但是對于去除的=要怎么補(bǔ)回去呢?為什么就要去除這個(gè)=呢?它有什么特殊的地方嗎?我想,有必要來看一下base64的原理:
base64原理 base64字符集base64是一種基于64個(gè)可打印字符來表示二進(jìn)制數(shù)據(jù)的方法,包括A-Z、a-z、0-9、+、/,以及一個(gè)補(bǔ)位用的=,實(shí)際上來看是65個(gè)字符,來看一張base64索引表:
基本原理:將每3個(gè)8字節(jié)轉(zhuǎn)換為4個(gè)6字節(jié),然后將轉(zhuǎn)換后的4個(gè)6字節(jié)高位添加2個(gè)0,組成4個(gè)8字節(jié),所以base64要比原字符串的大1/3左右,來看一個(gè)例子,如何將zpy轉(zhuǎn)化為base64編碼:
首先確定ascii碼,分別為122,112,121,由于只有兩個(gè)字符,所以后面的一個(gè)字符對應(yīng)的二進(jìn)制為00000000,于是構(gòu)成的24位為:
01111010 | 01110000 | 01111001
以六位為一個(gè)部分進(jìn)行分割:
011110 | 100111 | 000001 | 111001
對高位進(jìn)行補(bǔ)0的操作,再將其轉(zhuǎn)化為10進(jìn)制:
30 | 39 | 1 | 57
在結(jié)合上圖的索引表來看,很容易得出最終的值為enB5,由這個(gè)規(guī)則來看,我們很容易得出的是zp為enAA,但是利用new Buffer("zp").toString("base64")得出的為enA=,仔細(xì)來看base64還有兩項(xiàng)規(guī)則:
兩個(gè)字節(jié)的情況:將這兩個(gè)字節(jié)的一共16個(gè)二進(jìn)制位,按照上面的規(guī)則,轉(zhuǎn)成三組,最后一組除了前面加兩個(gè)0以外,后面也要加兩個(gè)0。這樣得到一個(gè)三位的Base64編碼,再在末尾補(bǔ)上一個(gè)"="號。
一個(gè)字節(jié)的情況:將這一個(gè)字節(jié)的8個(gè)二進(jìn)制位,按照上面的規(guī)則轉(zhuǎn)成二組,最后一組除了前面加二個(gè)0以外,后面再加4個(gè)0。這樣得到一個(gè)二位的Base64編碼,再在末尾補(bǔ)上兩個(gè)"="號。
解析tokenbase64的原理搞清楚了,那如何給token添加=也就很明白了,無外乎是在結(jié)尾加一個(gè)還是兩個(gè)。base64編碼長度一定是4的倍數(shù),所以只需要在%4之后進(jìn)行加=操作就好,結(jié)果只可能有兩種情況:
2,添加兩個(gè)=
3,添加一個(gè)=
所以可以寫出下面代碼來將token轉(zhuǎn)化為一個(gè)真正的base64:
const getBase64UrlUnescape = str => { str += new Array(5 - str.length % 4).join("="); return str.replace(/-/g, "+") .replace(/\_/g, "/"); };
到這里,解析方法就可以很容易的出來了:
const decodeBase64Url = str => JSON.parse( new Buffer(getBase64UrlUnescape(str), "base64").toString() ); jwt.decode = (token) => { const segments = token.split("."); return { header: decodeBase64Url(segments[0]), payload: decodeBase64Url(segments[1]) }; };驗(yàn)證
前面基本都理解了,驗(yàn)證其實(shí)很簡單,就是利用密鑰,來比對,看是否正確驗(yàn)證:
const verifyMethod = { hmac: (input, key, method, signStr) => signStr === sign(input, key, method, "hmac"), sign: (input, key, method, sign) => { return crypto.createVerify(method) .update(input) .verify(key, getBase64UrlUnescape(sign), "base64"); } };寫在最后
本文只是本人的一點(diǎn)愚見,如有錯(cuò)誤,歡迎大家指出。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/82434.html
摘要:什么是官方給出的定義是是一個(gè)開放標(biāo)準(zhǔn),它定義了一種緊湊的獨(dú)立的方式,用于安全地在當(dāng)事人之間傳遞信息作為一個(gè)對象。結(jié)構(gòu)由下面三個(gè)部分組成一個(gè)字符串通常由兩個(gè)部分組成令牌的,即,以及正在使用的散列算法,如或。 什么是JSON Web Token ? 官方給出的定義是:JSON Web Token(JWT)是一個(gè)開放標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊的、獨(dú)立的方式,用于安全地在當(dāng)事人...
摘要:用于方便地搭建能夠處理超高并發(fā)擴(kuò)展性極高的動(dòng)態(tài)應(yīng)用服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)。 介紹 權(quán)限認(rèn)證是接口開發(fā)中不可避免的問題,權(quán)限認(rèn)證包括兩個(gè)方面 接口需要知道調(diào)用的用戶是誰 接口需要知道該用戶是否有權(quán)限調(diào)用 第1個(gè)問題偏向于架構(gòu),第2個(gè)問題更偏向于業(yè)務(wù),因此考慮在架構(gòu)層解決第1個(gè)問題,以達(dá)到以下目的 所有請求被保護(hù)的接口保證是合法的(已經(jīng)認(rèn)證過的用戶) 接口可以從請求頭中獲取當(dāng)前用戶信息 每個(gè)...
摘要:做這個(gè)記錄之前,剛完成使用作為公司前端項(xiàng)目的持續(xù)交付工具的實(shí)踐,打算寫的教程前先把官方文檔扒下來做個(gè)翻譯站。在實(shí)踐一番后,卡在不能頻密調(diào)取翻譯這塊上,項(xiàng)目無法進(jìn)行下去。 做這個(gè)記錄之前,剛完成使用drone作為公司前端項(xiàng)目的持續(xù)交付工具的實(shí)踐,打算寫的教程前先把官方文檔扒下來做個(gè)翻譯站。在實(shí)踐一番后,卡在不能頻密調(diào)取google翻譯這塊上,項(xiàng)目無法進(jìn)行下去。最后覺得經(jīng)歷的過程涉及的內(nèi)容...
閱讀 2008·2019-08-29 16:27
閱讀 1377·2019-08-29 16:14
閱讀 3380·2019-08-29 14:18
閱讀 3461·2019-08-29 13:56
閱讀 1260·2019-08-29 11:13
閱讀 2128·2019-08-28 18:19
閱讀 3447·2019-08-27 10:57
閱讀 2283·2019-08-26 11:39