最近了解下基於 Token 的身份驗證,跟大伙分享下。很多大型網站也都在用,比如 Facebook,Twitter,Google+,Github 等等,比起傳統的身份驗證方法,Token 擴展性更強,也更安全點,非常適合用在 Web 應用或者移動應用上。Token 的中文有人翻譯成 “令牌”,我覺得挺好,意思就是,你拿着這個令牌,才能過一些關卡。
文章先介紹了一下傳統身份驗證與基於 JWT 身份驗證的方法,再理解一下 JWT 的 Token 的組成部分(頭部,數據,簽名),最后我們會在一個 Node.js 項目上實施簽發與驗證 JWT 的功能。練習的視頻版本可以參考《JWT:JSON Web Token》這個免費的課程,項目代碼在 Github 上可以找到。
寧皓網有一系列的基於 Token 身份驗證的課程,比如在 Node.js 項目里,或者 WordPress 網站上實現這種身份驗證的方法,我們還介紹了在小程序里面使用了這種基於 Token 的方法來驗證小程序用戶的身份。
訂閱寧皓網以后,就可以在線學習所有這些基於 Token 驗證身份的相關課程。
傳統身份驗證的方法
HTTP 是一種沒有狀態的協議,也就是它並不知道是誰是訪問應用。這里我們把用戶看成是客戶端,客戶端使用用戶名還有密碼通過了身份驗證,不過下回這個客戶端再發送請求時候,還得再驗證一下。
解決的方法就是,當用戶請求登錄的時候,如果沒有問題,我們在服務端生成一條記錄,這個記錄里可以說明一下登錄的用戶是誰,然后把這條記錄的 ID 號發送給客戶端,客戶端收到以后把這個 ID 號存儲在 Cookie 里,下次這個用戶再向服務端發送請求的時候,可以帶着這個 Cookie ,這樣服務端會驗證一個這個 Cookie 里的信息,看看能不能在服務端這里找到對應的記錄,如果可以,說明用戶已經通過了身份驗證,就把用戶請求的數據返回給客戶端。
上面說的就是 Session,我們需要在服務端存儲為登錄的用戶生成的 Session ,這些 Session 可能會存儲在內存,磁盤,或者數據庫里。我們可能需要在服務端定期的去清理過期的 Session 。
基於 Token 的身份驗證方法
使用基於 Token 的身份驗證方法,在服務端不需要存儲用戶的登錄記錄。大概的流程是這樣的:
- 客戶端使用用戶名跟密碼請求登錄
- 服務端收到請求,去驗證用戶名與密碼
- 驗證成功后,服務端會簽發一個 Token,再把這個 Token 發送給客戶端
- 客戶端收到 Token 以后可以把它存儲起來,比如放在 Cookie 里或者 Local Storage 里
- 客戶端每次向服務端請求資源的時候需要帶着服務端簽發的 Token
- 服務端收到請求,然后去驗證客戶端請求里面帶着的 Token,如果驗證成功,就向客戶端返回請求的數據
JWT
實施 Token 驗證的方法挺多的,還有一些標准方法,比如 JWT,讀作:jot ,表示:JSON Web Tokens 。JWT 標准的 Token 有三個部分:
- header(頭部)
- payload(數據)
- signature(簽名)
中間用點分隔開,並且都會使用 Base64 編碼,所以真正的 Token 看起來像這樣:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
每個 JWT token 里面都有一個 header,也就是頭部數據。里面包含了使用的算法,這個 JWT 是不是帶簽名的或者加密的。主要就是說明一下怎么處理這個 JWT token 。
頭部里包含的東西可能會根據 JWT 的類型有所變化,比如一個加密的 JWT 里面要包含使用的加密的算法。唯一在頭部里面要包含的是 alg 這個屬性,如果是加密的 JWT,這個屬性的值就是使用的簽名或者解密用的算法。如果是未加密的 JWT,這個屬性的值要設置成 none。
示例:
{ "alg": "HS256" }
意思是這個 JWT 用的算法是 HS256。上面的內容得用 base64url 的形式編碼一下,所以就變成這樣:
eyJhbGciOiJIUzI1NiJ9
Payload
Payload 里面是 Token 的具體內容,這些內容里面有一些是標准字段,你也可以添加其它需要的內容。下面是標准字段:
- iss:Issuer,發行者
- sub:Subject,主題
- aud:Audience,觀眾
- exp:Expiration time,過期時間
- nbf:Not before
- iat:Issued at,發行時間
- jti:JWT ID
比如下面這個 Payload ,用到了 iss 發行人,還有 exp 過期時間這兩個標准字段。另外還有兩個自定義的字段,一個是 name ,還有一個是 admin 。
{ "iss": "ninghao.net", "exp": "1438955445", "name": "wanghao", "admin": true }
使用 base64url 編碼以后就變成了這個樣子:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature
JWT 的最后一部分是 Signature ,這部分內容有三個部分,先是用 Base64 編碼的 header.payload ,再用加密算法加密一下,加密的時候要放進去一個 Secret ,這個相當於是一個密碼,這個密碼秘密地存儲在服務端。
- header
- payload
- secret
const encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
處理完成以后看起來像這樣:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后這個在服務端生成並且要發送給客戶端的 Token 看起來像這樣:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客戶端收到這個 Token 以后把它存儲下來,下回向服務端發送請求的時候就帶着這個 Token 。服務端收到這個 Token ,然后進行驗證,通過以后就會返回給客戶端想要的資源。
簽發與驗證 JWT
在應用里實施使用基於 JWT 這種 Token 的身份驗證方法,你可以先去找一個簽發與驗證 JWT 的功能包。無論你的后端應用使用的是什么樣的程序語言,系統,或者框架,你應該都可以找到提供類似功能的包。
下面我們在一個 Node.js 項目里,用最簡單的方式來演示一下簽發還有驗證 JWT 的方法。練習有個視頻版本,你可以參考《 JWT:JSON Web Token 》這個免費的視頻課程。
項目代碼:https://github.com/ninghao/jwt-demo
准備項目
准備一個簡單的 Node.js 項目:
cd ~/desktop mkdir jwt-demo cd jwt-demo npm init -y
安裝簽發與驗證 JWT 的功能包,我用的叫 jsonwebtoken,在項目里安裝一下這個包:
npm install jsonwebtoken --save
簽發 JWT
在項目里隨便添加一個 .js 文件,比如 index.js,在文件里添加下面這些代碼:
const jwt = require('jsonwebtoken') // Token 數據 const payload = { name: 'wanghao', admin: true } // 密鑰 const secret = 'ILOVENINGHAO' // 簽發 Token const token = jwt.sign(payload, secret, { expiresIn: '1day' }) // 輸出簽發的 Token console.log(token)
非常簡單,就是用了剛剛為項目安裝的 jsonwebtoken 里面提供的 jwt.sign 功能,去簽發一個 token。這個 sign 方法需要三個參數:
- playload:簽發的 token 里面要包含的一些數據。
- secret:簽發 token 用的密鑰,在驗證 token 的時候同樣需要用到這個密鑰。
- options:一些其它的選項。
在命令行下面,用 node 命令,執行一下項目里的 index.js 這個文件(node index.js),會輸出應用簽發的 token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzM5MDYsImV4cCI6MTUyOTEyMDMwNn0.DctA2QlUCrM6wLWkIO78wBVN0NLpjoIq4T5B_2WJ-PU
上面的 Token 內容並沒有加密,所以如果用一些 JWT 解碼功能,可以看到 Token 里面包含的內容,內容由三個部分組成,像這樣:
// header { "alg": "HS256", "typ": "JWT" } // payload { "admin": true, "iat": 1529033906, "name": "wanghao", "exp": 1529120306 } // signature DctA2QlUCrM6wLWkIO78wBVN0NLpjoIq4T5B_2WJ-PU
假設用戶通過了某種身份驗證,你就可以使用上面的簽發 Token 的功能為用戶簽發一個 Token。一般在客戶端那里會把它保存在 Cookie 或 LocalStorage 里面。
用戶下次向我們的應用請求受保護的資源的時候,可以在請求里帶着我們給它簽發的這個 Token,后端應用收到請求,檢查簽名,如果驗證通過確定這個 Token 是我們自己簽發的,那就可以為用戶響應回他需要的資源。
驗證 JWT
驗證 JWT 的用效性,確定一下用戶的 JWT 是我們自己簽發的,首先要得到用戶的這個 JWT Token,然后用 jwt.verify這個方法去做一下驗證。這個方法是 Node.js 的 jsonwebtoken 這個包里提供的,在其它的應用框架或者系統里,你可能會找到類似的方法來驗證 JWT。
打開項目的 index.js 文件,里面添加幾行代碼:
// 驗證 Token jwt.verify(token, 'bad secret', (error, decoded) => { if (error) { console.log(error.message) return } console.log(decoded) })
把要驗證的 Token 數據,還有簽發這個 Token 的時候用的那個密鑰告訴 verify 這個方法,在一個回調里面有兩個參數,error 表示錯誤,decoded 是解碼之后的 Token 數據。
執行:
node ~/desktop/jwt-demo/index.js
輸出:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzQ3MzMsImV4cCI6MTUyOTEyMTEzM30.swXojmu7VimFu3BoIgAxxpmm2J05dvD0HT3yu10vuqU
invalid signature
注意輸出了一個 invalid signature ,表示 Token 里的簽名不對,這是因為我們組長 verify 方法提供的密鑰並不是簽發 Token 的時候用的那個密鑰。這樣修改一下:
jwt.verify(token, secret, (error, decoded) => { ...
再次運行,會輸出類似的數據:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzUzODYsImV4cCI6MTUyOTEyMTc4Nn0.mkNrt4TfcfmP22xd3C_GQn8qnUmlB39dKT9SpIBTBGI { name: 'wanghao', admin: true, iat: 1529035386, exp: 1529121786 }
RS256 算法
默認簽發還有驗證 Token 的時候用的是 HS256 算法,這種算法需要一個密鑰(密碼)。我們還可以使用 RS256 算法簽發與驗證 JWT。這種方法可以讓我們分離開簽發與驗證,簽發時需要用一個密鑰,驗證時使用公鑰,也就是有公鑰的地方只能做驗證,但不能簽發 JWT。
在項目下面創建一個新的目錄,里面可以存儲即將生成的密鑰與公鑰文件。
cd ~/desktop/jwt-demo
mkdir config
cd config
密鑰
先生成一個密鑰文件:
ssh-keygen -t rsa -b 2048 -f private.key
公鑰
基於上面生成的密鑰,再去創建一個對應的公鑰:
openssl rsa -in private.key -pubout -outform PEM -out public.key
簽發 JWT(RS256 算法)
用 RS256 算法簽發 JWT 的時候,需要從文件系統上讀取創建的密鑰文件里的內容。
const fs = require('fs') // 獲取簽發 JWT 時需要用的密鑰 const privateKey = fs.readFileSync('./config/private.key')
簽發仍然使用 jwt.sign 方法,只不過在選項參數里特別說明一下使用的算法是 RS256:
// 簽發 Token const tokenRS256 = jwt.sign(payload, privateKey, { algorithm: 'RS256' }) // 輸出簽發的 Token console.log('RS256 算法:', tokenRS256)
驗證 JWT(RS256 算法)
驗證使用 RS256 算法簽發的 JWT,需要在文件系統上讀取公鑰文件里的內容。然后用 jwt 的 verify 方法去做驗證。
// 獲取驗證 JWT 時需要用的公鑰 const publicKey = fs.readFileSync('./config/public.key') // 驗證 Token jwt.verify(tokenRS256, publicKey, (error, decoded) => { if (error) { console.log(error.message) return } console.log(decoded) })