JWT是 JSON Web Tokens 的縮寫。
最近工作上需要用 JWT 來互傳資訊,
覺得這東西用起來是滿簡單的。
只是結合多個概念,一開始不是很好懂,紀錄下理解的過程。
最後再介紹利用 jwt
這個 gem 來制作 jwt
TL;DR
個人觀點,不要打我 xD
- 可把 JWT 當成 JSON
- JWT 是無法修改的 JSON
- JWT 跟加密無關
參考資料
- JSON Web Tokens
- GitHub - jwt/ruby-jwt: A ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.
Why?
先不提 JWT 怎麼實做的,先想像班上有一對情侶 Dustin 跟 Suzie...
兩人都是 Geek,
所以在教室最後一排的 Dustin 要傳紙條給第一排的 Suzie 的時候,
竟然是用 JSON 的格式!
{
"from": "Dustin",
"to": "Suzie",
"message": "Do you want to have dinner with me tonight?",
"place": "MacDonald's"
}
不過傳紙條的過程中,
班上就是會有人硬要打開不是給他的紙條,
比如 Will 就是打開紙條並把內容改成
{
"from": "Dustin",
"to": "Erica", // 被 Will 改了
"message": "Do you want to have dinner with me tonight?",
"place": "MacDonald's"
}
於是不僅當晚 Dustin 只能孤零零一個人吃大麥克,
Suzie 還不理他一個禮拜,悲劇。
其實一切就是他沒辦法控制發出去的訊息會不會被篡改。
後來 Dustin 記取教訓,
事先跟 Suzie 約定好一個暗號 NeverEndingStory
並將訊息用HMAC SHA-256
搭配NeverEndingStory
這個暗號製作一串亂碼,
先直接接受
HMAC SHA-256
搭配相同的暗號跟訊息,會產出唯一的亂碼
再把亂碼寫在紙條的最下面
796e0c718cc2768edfb67a53b0f4fed74b4abbac61baaa68876630d9827714a0
Suzie 打開紙條後即可以用暗號NeverEndingStory
以相同的方式將紙上的訊息轉成亂碼,
再檢查和紙上附的亂碼是否一致, 就知道訊息是完整且沒被修改過。
這樣就可以避免再次發生悲劇了。
可以任意找一個線上的 HMAC SHA256 轉換器來驗證 Free HMAC-SHA256 Online Generator Tool | Devglan
只要訊息跟算出來的亂碼不合,即知道訊息已遭到修改或者不完全。
所以這樣產生出來的亂碼很常被叫做數位簽名
JWT 就是給 JSON 加上一個簽名,確保訊息沒有被任何人動過。
變得好像可以宣告一個 const 的 JSON 再傳送出去一樣,
JWT的組成
- Header: 註明是用何種演算法製作簽名的
- Payload: 就是實際訊息的 JSON
- Signature: 利用 Header 註明的演算法用 HMAC 方式製作出來簽名
可以到 JSON Web Tokens - jwt.io製作一個 JWT 試試看
左邊即為製作出來的 JWT。
我想這時已經有人握緊拳頭了...
聽你鬼扯!
這個 JWT 看起來根本就只是一串亂碼!
什麼 JSON 、指定的演算法跑到哪裡去了?
上面的確是漏說了一些細節 😂
Header, Payload 其實是會先經由 base64 去編碼
base64 就是個編碼的方式,可理解成一個可編碼及還原的方法。
可參考上篇文章
所以紅色部色就是header,紫色部分就是 JSON
這邊用 ruby 來試著驗證看看
header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
payload = 'eyJuYW1lIjoiS2V2aW4ifQ'
# 用 base64 去解碼,可得到原始內容
require 'base64'
puts Base64.urlsafe_decode64(header)
# {"alg":"HS256","typ":"JWT"}
puts Base64.urlsafe_decode64(payload)
# {"name":"Kevin"}
# 用 JWT 的方式,把 header 跟 payload 連起來
# 用 HMAC SHA256 的方式產生簽名
require 'openssl'
mac = OpenSSL::HMAC.hexdigest("SHA256", 'mySecret', "#{header}.#{payload}")
Base64.urlsafe_encode64(mac).gsub('=', '') # '=' is just padding
# "IMa4S4W1LMP1xuKVglwBagrHA5wwK9sBu-CVDKudIkg"
# 跟網站上產出的一樣喔,表示訊息沒被動過
可能會疑惑怎麼可以直接把 payload 還原成 JSON,
那 JWT 裡的資料不就大辣辣地秀出,這樣不是不安全?
沒錯...因為 JWT 好像變成一串亂碼,容易誤會它很安全,
其實它跟加密完全沒有關係 xD
(加密完全是 TLS 也就是 https 的事,先忽略吧)
JWT 主要在乎資料是否被篡改, Signature 是否一致而已
所以別把敏感的資訊放在裡面。
我們可以先看實際使用 JWT 的程式碼
Demo
因為我主要用 ruby,這邊利用 jwt
這個 gem 來demo。
不過幾乎所有的語言都有實作 jwt,
可在 JSON Web Tokens - jwt.io 查找相關資源。
require 'jwt'
payload = {
first_name: 'Kevin',
last_name: 'Luo'
}
secret = "my secret"
token = JWT.encode payload, secret, 'HS256'
# "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdF9uYW1lIjoiS2V2aW4iLCJsYXN0X25hbWUiOiJMdW8ifQ.dZJnejsQ9cWs1hyOvCAij_Q4k87vfbQpeBIjgqYCrgs"
decoded_token = JWT.decode token, secret, true, { algorithm: 'HS256' }
# [{"first_name"=>"Kevin", "last_name"=>"Luo"}, {"alg"=>"HS256"}]
此外,JWT的格式RFC 其實有約定一些參數可以設定,
不過端看程式有沒有做對應的處理。
一個常用的是「到期時間」 exp
,設定一個到期時間給 JWT, 假使真的到期,decode 時即丟出JWT::ExpiredSignature
這個 Exception。
require 'jwt'
exp = Time.now.to_i + 3600 # 1 hour
exp_payload = { data: payload, exp: exp }
token = JWT.encode exp_payload, secret, 'HS256'
# "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImZpcnN0X25hbWUiOiJLZXZpbiIsImxhc3RfbmFtZSI6Ikx1byJ9LCJleHAiOjE2MTM4ODU4MjF9.1_NIKXDnBVz1G6Li7_CZbcDwIk5AFaOsreK7BFDS13Q"
begin
decoded_token = JWT.decode token, secret, true, { algorithm: ‘HS256’ }
# [{"data"=>{"first_name"=>"Kevin", "last_name"=>"Luo"}, "exp"=>1613885821}, {"alg"=>"HS256"}]
rescue JWT::ExpiredSignature
# 過期
end
應用
JWT 除了可以讓 2 台有共同 secret 的電腦可互傳確認不會被篡改的資料外,
最常見的情境應該就是後端 API 使用者登入後發給前端的 session token 了吧。
會用 API 的 Request 的 Header 中所帶的 token,來判斷來源是否可以取用該 API
雖然拿 JWT 當 token,因為前端應該是不會有共有的 secret,
沒辦法去驗證訊息,所以對前端而言它只是一串亂碼。
不過我覺得用JWT當 session token 應該有 2 個優點:
- 由於 JWT 有不會被篡改特性,server 收到 token 後,可在裡面直接取用資料,比如說 user_id 之類的。所以才說見令如見人
- 不同使用者的 JWT 因為 user_id 不同,必長得不同,而不用是檢查碰撞。如果 token 是隨機產生,我們還得去檢查是否有碰撞(就是2個使用者運氣好,用到相同的亂數字串)
目前就只用過這 2 種應用,但不會改篡改的特性有很多想像空間,不知道其它人曾拿它來做什麼事囉?
JWT 的分享到此囉 : )
Top comments (4)
我想請問,如果 server 在使用者登入後、回傳一個 token、之後的 API 都使用這個 token 就好,那請問要怎麼驗證呢?是每次 request 進來的時候、都要去資料庫拉出資料來驗證嗎?
另外,若是使用者洩漏出該 token,通常是採取什麼機制來防範?是登出後使所有 token 無效(但這樣資料庫得存),或是再另外做一個更改密碼或冷凍帳戶的 API?
假如該 token 是用 JWT 的話,server 端可以直接看到 jwt 的內容跟簽名。所以用jwt 的內容再生成那個一次簽名,比對一下2個簽名是否一樣就好了,接下來看 server 想做什麼事。
雖然上面說方式是不會用到資料庫的資料,但我覺得總是會重新從資料庫抓一下那個使用者的資料,哈哈。從資料庫中有 primary key 的情況下抓一筆資料,應該是 1 ms 的事,因為我並沒有處理會無法容忍 1ms 延遲的系統...我覺得每次多一個 query 無妨...
洩漏的話,先假設是使用者洩漏好了。讓使用者去改密碼,改完後[ 最後更新時間」的欄位也順便更新一下。那 JWT 可帶入使用者的「更新時間」,但這樣每次要去比對使用者的資料。另一的方法就是直接改生成簽名的secret,這樣所有的 token 都失效,不過這樣會讓所有人登出,或每個使用者都有自己的 secret。
我感覺 authentication 的方法並沒有考慮使用者「自行洩漏」token 的問題,所以依實際情形想出一個方法可以解決就好了
感謝回應。想請問關於 "直接改生成簽名的 Secret",這件事的意思是,資料庫中會有個欄位紀錄使用者的 Secret,每次生成 JWT 時都會使用該 Secret 嗎?思索了一下,在每次登出或修改密碼時更新 Secret 的確能夠避免被他人利用 Token 使用帳號的場景。
恩恩,我們似乎討論出了一種解法 xDD
我的確是指 users 的 table 直接多一個欄位叫 secret。
我又想到如果配合JWT的過期時間,每次生成 JWT 之前都亂數產生一個 secret ,這樣的話:
有點像現在流行的 OTP(每30秒變一次的那種密碼), 但感覺關鍵點會是如何做到無縫更新 token