見令如見人，介紹 JWT
Tags: ruby, chinese, jwt
Published: 2021-02-21 15:26:33

JWT是 [JSON Web Tokens](https://jwt.io/) 的縮寫。
最近工作上需要用 JWT 來互傳資訊，
覺得這東西用起來是滿簡單的。
只是結合多個概念，一開始不是很好懂，紀錄下理解的過程。
最後再介紹利用 `jwt` 這個 gem 來制作 jwt

# TL;DR
個人觀點，不要打我 xD
* 可把 JWT 當成 JSON
* JWT 是無法修改的 JSON
* JWT 跟加密無關

# 參考資料
* [JSON Web Tokens](https://jwt.io/)
* [GitHub - jwt/ruby-jwt: A ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.](https://github.com/jwt/ruby-jwt)

# Why?
先不提 JWT 怎麼實做的，先想像班上有一對情侶 Dustin 跟 Suzie...
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rod1dsga7t1ebkxc6vwx.png)
兩人都是 Geek，
所以在教室最後一排的 Dustin 要傳紙條給第一排的 Suzie 的時候，
竟然是用 JSON 的格式！
```json
{
  "from": "Dustin",
  "to": "Suzie",
  "message": "Do you want to have dinner with me tonight?",
  "place": "MacDonald's"
}
```
不過傳紙條的過程中，
班上就是會有人硬要打開不是給他的紙條，
比如 Will 就是打開紙條並把內容改成
```json
{
  "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](https://www.devglan.com/online-tools/hmac-sha256-online)
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1jvajt3d8x2338u2ofg.png)

**只要訊息跟算出來的亂碼不合，即知道訊息已遭到修改或者不完全**。
所以這樣產生出來的亂碼很常被叫做**數位簽名**

JWT 就是給 JSON 加上一個**簽名**，確保訊息沒有被任何人動過。
變得好像可以宣告一個 const 的 JSON 再傳送出去一樣，


# JWT的組成
JWT最後的呈現是一串字串，有3個部分，以 `.` 分開
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fk6o3ubsrqn5gxdhgeji.png) 
* Header: 註明是用何種演算法製作簽名的
* Payload: 就是實際訊息的 JSON
* Signature: 利用 Header 註明的演算法用 HMAC 方式製作出來簽名

可以到 [JSON Web Tokens - jwt.io](https://jwt.io/)製作一個 JWT 試試看
![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xhvtusgwlxlb1yyhklj.png)
左邊即為製作出來的 JWT。

我想這時已經有人握緊拳頭了...
> 聽你鬼扯！
> 這個 JWT 看起來根本就只是一串亂碼！
> 什麼 JSON 、指定的演算法跑到哪裡去了？

上面的確是漏說了一些細節 😂
Header, Payload 其實是會先經由 base64 去編碼
base64 就是個編碼的方式，可理解成一個可編碼及還原的方法。
可參考上篇文章 {% post https://dev.to/kevinluo201/gem-base64-1jjp %}

所以紅色部色就是header，紫色部分就是 JSON
這邊用 ruby 來試著驗證看看
```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](https://jwt.io/) 查找相關資源。
```ruby
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。
```ruby
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 的分享到此囉 : )