1. 程式人生 > >基於 Token 的身份驗證:JSON Web Token

基於 Token 的身份驗證:JSON Web Token

最近了解下基於 Token 的身份驗證,跟大夥分享下。很多大型網站也都在用,比如 Facebook,Twitter,Google+,Github 等等, 比起傳統的身份驗證方法,Token 擴充套件性更強,也更安全點,非常適合用在 Web 應用或者移動應用上。Token 的中文有人翻譯成 “令牌”,我覺得挺好,意思就是,你拿著這個令牌,才能過一些關卡。

——

文章先介紹了一下傳統身份驗證與基於 JWT 身份驗證的方法,再理解一下 JWT 的 Token 的組成部分(頭部,資料,簽名),
最後我們會在一個 Node.js 專案上實施簽發與驗證 JWT 的功能。練習的視訊版本可以參考《

JWT:JSON Web Token》這個免費的課程,專案程式碼在 Github 上可以找到。

傳統身份驗證的方法

HTTP 是一種沒有狀態的協議,也就是它並不知道是誰是訪問應用。這裡我們把使用者看成是客戶端,客戶端使用使用者名稱還有密碼通過了身份驗證,不過下回這個客戶端再發送請求時候,還得再驗證一下。

解決的方法就是,當用戶請求登入的時候,如果沒有問題,我們在服務端生成一條記錄,這個記錄裡可以說明一下登入的使用者是誰,然後把這條記錄的 ID 號傳送給客戶端,客戶端收到以後把這個 ID 號儲存在 Cookie 裡,下次這個使用者再向服務端傳送請求的時候,可以帶著這個 Cookie ,這樣服務端會驗證一個這個 Cookie 裡的資訊,看看能不能在服務端這裡找到對應的記錄,如果可以,說明使用者已經通過了身份驗證,就把使用者請求的資料返回給客戶端。

上面說的就是 Session,我們需要在服務端儲存為登入的使用者生成的 Session ,這些 Session 可能會儲存在記憶體,磁碟,或者資料庫裡。我們可能需要在服務端定期的去清理過期的 Session 。

基於 Token 的身份驗證方法

使用基於 Token 的身份驗證方法,在服務端不需要儲存使用者的登入記錄。大概的流程是這樣的:

  1. 客戶端使用使用者名稱跟密碼請求登入
  2. 服務端收到請求,去驗證使用者名稱與密碼
  3. 驗證成功後,服務端會簽發一個 Token,再把這個 Token 傳送給客戶端
  4. 客戶端收到 Token 以後可以把它儲存起來,比如放在 Cookie 裡或者 Local Storage 裡
  5. 客戶端每次向服務端請求資源的時候需要帶著服務端簽發的 Token
  6. 服務端收到請求,然後去驗證客戶端請求裡面帶著的 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

示例:

{
  "typ": "JWT",
  "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 編碼以後就變成了這個樣子:

	eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

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 方法需要三個引數:

  1. playload:簽發的 token 裡面要包含的一些資料。
  2. secret:簽發 token 用的金鑰,在驗證 token 的時候同樣需要用到這個金鑰。
  3. 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,需要在檔案系統上讀取公鑰檔案裡的內容。然後用 jwtverify 方法去做驗證。

	// 獲取驗證 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)
	})

文章轉載於: https://ninghao.net/blog/2834

基於 Token 的身份驗證:JSON Web Token