以 JSON Web Token 替代傳統 Token
前後端工程師,善用 JavaScript、HTML 5、CSS 3 和 Golang 與 PHP、Node.js 、C#。目前正為台灣的社群網站進行趕工,然而這東西卻趕了很久。

JSON Web Token (JWT) 是由 Auth0 所提構出的一個新 Token 想法,這並不是一套軟體、也不是一個技術,如果你在做網站時有用 Token 驗證使用者身份的習慣,那麼這個方法你應該很快就能上手。本篇將解說為什麼 JWT 會比起傳統 Token 要來得好。

在傳統網站中我們會以 Session 來辨別使用者是否有登入,由於 Session 只會被伺服端知道,所以我們就可以 Session 中擺放一些機密資訊並且供之後驗證用。

為什麼大家不用 Session 了?

隨著網路的擴展,Session 有個問題,那就是具狀態性(Stateful)還有容易受跨網域請求偽造攻擊(CSRF Attack)。先讓我們先以具狀態性的問題為例。

極具狀態性

假設今天有兩台伺服器透過負載平衡來相互處理使用者請求,由於 Session 是儲存在伺服端上的,第一次使用者登入時是由 Server 1 處理,那麼這個 Session 自然也就儲存在 Server 1。

但下次負載平衡指派使用者到 Server 2 的時候呢?這個 Session 也就不存在,所以使用者就需要重新登入一次(雖然有辦法可解決,但暫不討論)。

易受跨網域請求偽造攻擊

由於 Session 是儲存在伺服端的,這意味著客戶端在發送請求時幾乎不用提供什麼資訊,對吧?如果今天有人發送了一個刪除文章的連結給你,然後你在不知情的情況下就按下去的時候會發生什麼事情?你的文章會這樣被刪除掉。

為此,有人想出了 Token 來解決這個問題,並且能夠在多個伺服器上跨域使用。

Token 解決了什麼?

當使用者登入成功時,他會得到一串看起來毫無意義的亂數字串,但這個字串實際上會對應到資料庫中使用者的身份,簡單說就是另類的帳號,這也被叫做 Token。

無狀態性

Token 本身是不帶資訊的且無狀態性的(Stateless),當伺服器接收到 Token 時,會主動去對應使用者的資料表,接著就能夠知道這個 Token 代表著哪個使用者,然後抓出相關的資訊來使用。

由於多個伺服器都是共享一個資料庫的,所以我們的伺服器現在不會有 Session 那樣的問題。但需要注意的是 Token 不可以被別人知道,否則別人就能夠擁有你的身份、偽造成你。

安全性提高

由於使用者現在必須主動提供 Token,也順帶解決了跨網域請求偽造攻擊,如果今天朋友再次給了你刪除文章的連結,由於這個連結並不帶有 Token,伺服器也就不能知道是誰想要刪除文章,所以這個動作自然就會變成無效。

為什麼該用 JWT 取代傳統 Token?

你能夠直接在 JWT 中存放資料,而不用額外呼叫資料庫。

當我們使用傳統 Token 時,我們需要呼叫資料庫並且比對這一個 Token 是誰的,然後我們才能夠取得該使用者的資料。當有大量使用者湧入時,這可能會令伺服器負荷不堪。

而 JWT 解決了這個問題,因為我們可以直接把使用者資料存放在「Token」中,所以也就省去了額外的資料庫開銷。且在微服務架構中也能夠方便地分享使用者資料。

到這裡你可能會開始想:「這樣不是很危險嗎?」、「如果使用者自行修改了 Token 該怎麼辦?」,而我們接下來就要講解什麼是 JWT。

什麼是 JSON Web Token(JWT)?

先看看 JWT 的長相吧。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

上面是一個來自官方網站範例,我們將以此做為講解。

JWT 的構成

JWT 實際上是由三個部分組成的。

標頭.內容.簽名

標頭

JWT 的標頭包含了兩個部分,一個是加密類型(alg),另一個則是定義類型(typ)。

{
  "alg": "HS256",
  "typ": "JWT"
}

而這些內容通過 Base64 轉化就能夠得到我們的第一段字串。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

內容

內容是一個自定義的地方,你可以在裡面擺置使用者的帳號、暱稱,這樣你就不需要再去資料庫撈,這聽起來很不安全,但是不用擔心。

不過需要注意的是這些資料並沒有被「加密」,使用者可以直接看到這些資訊,所以不推薦在這裡擺放信用卡資料、密碼。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

這部分透過 Base64 轉化也就能得到以下字串。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

簽名

這就是好玩的地方了,當我們要簽發一個 JWT 的時候,我們會用一組密碼來簽名,這就是為了避免有人自己更改內容,然後簽發一個根本不是我們所產生的 JWT 來欺瞞伺服端。

這部分的算法是由上方兩個區塊的 Base64 以點(.)符號組合起來,並以密碼(在這裡是 secret)加密。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), "secret") 

然後我們就會得到簽名。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

接著組合起來就是 JWT 本體。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

由於 secret 這個密碼是儲存在伺服端的,所以也就沒有人能夠知道(除非暴力破解)。任何人都可以修改 JWT 的內容,但是當他簽發的時候並不知道密碼,所以就會有不對的簽名,伺服端也就自然不會接受這個錯誤的 JWT。

使用 JWT 不僅能夠節省伺服端的資料庫連線開銷,又能夠在資料分享上變得更加便利。