Token 放 localStorage?sessionStorage?還是 Cookie?

不要再把 JWT Token 放在 localStorage 了

前言

這一篇文章來分享與記錄一下關於 JWT Token 的一些事情,以及為什麼不要把 JWT Token 放在 localStorage。

JSON Web Token(JWT)

那麼這邊也簡單描述一下什麼是 JSON Web Token(JWT) 呢?基本上 JWT 是一個基於 JSON 標準的格式,那…它常見在哪些場景呢?

基本上需要授權的地方都可以使用 JWT,例如…

  • 登入
  • 重設密碼
  • 修改個人資料

等等。

因此當使用者登入的時候,你就會得到一組 JWT Token

Token

而這一組 Token 就會是你未來跟伺服器溝通的識別碼,而伺服器也會透過這一組 Token 來驗證你的身份。

JWT Token 的組成

接下來我們來快速聊一下關於 JWT 的組成,底下是 JWT 官方網站所提供的範例 Token

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT Token 基本上是由三個部分組合而成,分別是…

  • HEADER(標頭):ALGORITHM & TOKEN TYPE
    • 用來說明這一組 Token 是用什麼演算法加密的,以及 Token 的類型
  • PAYLOAD(負載):DATA
    • 主要是用來存放一些資料,例如使用者的 ID、名字等等
  • VERIFY SIGNATURE(驗證簽名)
    • 這一部分是用來驗證 Token 是否為正確的 Token

那麼我們該如何看出 HEADER、PAYLOAD 與 VERIFY SIGNATURE 呢?基本上 JWT Token 是使用 . 來分隔的,因此我們可以透過 . 來切割 Token,拆成以下:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. # HEADER
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # VERIFY SIGNATURE

到目前為此,應該已經對於 JWT Token 的組成有一定的概念了,所以接下來就來快速聊一下瀏覽器的儲存空間吧。

localStorage?sessionStorage?cookie?

當我們登入之後會取得一組 JWT Token 作為與後端伺服器溝通的識別碼,那麼再往下介紹之前,我想先快速重溫一下關於瀏覽器儲存資料的地方。

底下我列出幾個常見的地方:

  • localStorage
  • sessionStorage
  • cookie

接下來我們先來認識一下 localStoragesessionStorage,這兩個都是 HTML5 所提供的 Web Storage API,而這兩個的差別在於 localStorage 的資料會永久(或半永久,因為如果使用者刪除的話)儲存在瀏覽器中,而 sessionStorage 的資料只會存在於當前的瀏覽器分頁中。

接著我們來看一下 localStorage 的基本用法

1
2
3
4
5
6
const data = {
name: 'Ray',
url: 'https://israynotarray.com/',
};

localStorage.setItem('data', JSON.stringify(data));

透過上面程式碼,我們可以將資料儲存到瀏覽器的儲存空間內,因此你可以在開發者工具中找到它

localStorage

那麼 sessionStorage 的用法也是類似的,只是將 localStorage 改成 sessionStorage 即可

1
2
3
4
5
6
const data = {
name: 'Ray',
url: 'https://israynotarray.com/',
};

sessionStorage.setItem('data', JSON.stringify(data));

而且它也可以在開發者工具中找到,只是位置就不會在 localStorage 的位置,而是瀏覽階段儲存空間中

sessionStorage

那麼 sessionStoragelocalStorage 最大不同的地方在於,當你的瀏覽器分頁關閉之後 sessionStorage 的資料就會被清除,而 localStorage 的資料則會一直存在於瀏覽器中。

接下來我們接著看 Cookie 的寫法,Cookie 也是拿來儲存一些資料的,而儲存方式主要是採用 key=value 的方式,並使用 ; 來分隔不同的資料,例如

1
name=Ray;

因此寫入時,就可以這樣寫入

1
document.cookie = 'name=Ray';

Cookie

那麼上面就是我們最基本的 localStoragesessionStoragecookie 的用法。

(當然上面還有很多細節沒提到,為了讓我可以後面有東西講,所以我特別簡化了。)

JWT Token 該放…?

那麼所以該把 JWT Token 放在哪裡呢?透過前面的範例講解之後,其實我們可以知道 JWT Token 是一個非常重要的識別碼,我們要跟後端拿任何資料都會需要它,因此這一組 Token 是不可以隨意外流的,如果被不法之徒拿到的話,就有可能造成資料外洩的問題。

但是你知道嗎?看似加密的 JWT Token 其實是可以反向解析的,也就是說如果有人拿到了 JWT Token 就可以透過解析的方式來看到/取得裡面的資料。

舉例來講,以官方所提供的 JWT Token 為例:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

你是可以在官方網站上面直接解析,而且它還會告訴你這個 JWT Token 的加密方式是什麼,以及裡面的資料是什麼

Decoded JWT

你可以看到 PAYLOAD 直接顯示著以下資料

  • sub:代表著 Subject,也就是使用者的 ID
  • name:代表著使用者的名字
  • iat:代表著 Issued At,也就是這個 JWT Token 的發行時間

當然 PAYLOAD 的內容是可以由後端開發者決定的,因此你可以在裡面放任何資料,例如你為了方便前端呈現使用者的 Email 或是使用者的姓名等等。

那麼這邊就簡單示範一下,底下我寫了一個簡單的 Express+JWT 範例給予參考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const jwt = require('jsonwebtoken');

// ...略過一些程式碼

router.post('/login', function(req, res, next) {
const { username, password } = req.body;
if (username === 'admin' && password === '123456') {
const token = jwt.sign({
username,
email: 'example.com',
}, 'privateKeyTest', { expiresIn: 60 });

return res.send({
token,
message: '登入成功'
});
}

return res.status(401).send({
message: '登入失敗',
status: false,
});
});

// ...略過一些程式碼

當我們輸入正確的帳號密碼之後,就會拿到一組 JWT Token,而這組 JWT Token 的內容就會是以下這樣

1
2
3
4
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZW1haWwiOiJleGFtcGxlLmNvbSIsImlhdCI6MTY4NDIzNzAzNSwiZXhwIjoxNjg0MjM3MDk1fQ.y0KIWSmTSb4Mtwc6jbi0kejuka8B68p34zzNDz3YdVE",
"message": "登入成功"
}

接著你再拿這一組 Token 去解析,就可以得到以下結果

decoded

而你可能就會將這一組 Token 儲存到 localStorage 裡面,並且在每次發送請求的時候都帶上這一組 Token,讓後端可以透過這一組 Token 來判斷你的身份,這樣就可以達到驗證的效果,到目前為止這邊都看似沒有什麼太大的問題。

以我個人立場來講,我認為…

「將 Token 儲存在 localStorage 中並不是一件好事。」

為什麼呢?假設駭客可以在你的網站在執行 JavaScript,那麼也就代表著駭客可以透過 JavaScript 來取得你的 localStorage 裡面的資料,那麼這樣就會有可能造成資料外洩的問題。

底下這邊我寫了一個範例:

你可以看到上面的範例中,我在 CodePen 額外注入了一個 CodePen iFrame,然後直接透過 JavaScript 輕輕鬆鬆地取得 localStorage 的資料。

雖然 localStorage 會依據網域來做區分,但是如果你的網站有 XSS 的漏洞,那麼駭客就可以透過 XSS 的漏洞來取得你的 localStorage 裡面的資料並傳送給自己,那 sessionStorage 呢?其實它的狀況跟 localStorage 是差不多的,所以就不額外示範了。

那麼 localStoragesessionStorage 都會有這樣的問題,所以使用 Cookie 就可以解決嗎?

答案是不行,因為 Cookie 也有可能會被 XSS 的攻擊給竊取

講那麼多好像不論放 localStorage、sessionStorage 或是 Cookie 好像都會有被竊取的問題?所以接下來這邊我會講四個結論,來盡可能避免這些狀況的發生。

第一個結論:小心第三方資源的引用

基本上只要你的網站上可以執行第三方的 JavaScript,那麼你的資料就有可能會被竊取,這也是為什麼我們會盡可能避免使用 CDN 的原因之一,因為你無法確定 CDN 的資源是否安全,所以不論第三方的東西是否安全,只是出現第三方的東西,那麼就有可能會發生資料外洩的問題。

更不用說如果 CDN 突然掛掉、更新版本等問題。

第二個結論:遵守原則

正常來講是可以使用 HTTP Only 的 Cookie 來解決,只要有被設置成 HTTP Only 的 Cookie,那麼就不會被 JavaScript 給取得,但是由於現今開發都是採用前後端分離的架構,而 HTTP Only 又必須透過後端來設定,因此就會無法設定 HTTP Only 的 Cookie。

(如果沒有前後端分離的話,那麼也可以透過後端來設定 HTTP Only 的 Cookie 的。)

所以如果想要解決無法設置 HTTP Only 的問題的話,就會建議遵守以下原則

  • 不將敏感資訊放進 JWT 中,如:密碼、信用卡號等
  • 使用 HTTPS 傳輸協議
  • 使用短期 Token
  • 設置 CORS 的白名單,只允許特定的網域來存取 API
  • 使用 CSRF Token 來防止 CSRF 攻擊
  • 使用一些語法時,要注意 XSS 的問題,如:innerHTMLeval

基本上遵守以上原則就可以避免大部分的問題。

為什麼不建議將 Token 放置在 localStorage 裡面呢?因為 localStorage 是屬於半永久的儲存空間所以並不會隨著時間到期而消失,因此如果你的 Token 不會過期的話,那麼隨著時間拉長,那麼就會有隱藏的風險存在,更不用說你可能在裡面存放一些敏感資料的問題。

雖然 localStorage 在設計上是非常方便的,但是跟 Cookie 相比,反而會建議使用 Cookie,因為 Cookie 有額外提供一些選項可以讓你設定 Cookie 的有效期限,因此你可以透過這個有效期限來讓 Cookie 在一定時間之後就會消失,這樣也不用擔心 Token 會永遠存在的問題。

當然你也可以刻意與後端設置不同過期時間,例如:後端 Token 設置 7 天的有效期限,那麼你就可以設置 Cookie 的有效期限比後端更短,例如設置 3 天,這也是一種解決方案,而且你也可以設置該 Cookie 只能在特定的網域下才能存取,當然也可以增加 Refresh Token 機制。

只是這邊要注意 localStorage 的儲存空間大小為 5MB,而 Cookie 的儲存空間大小為 4KB,因此如果你的 Token 過大的話,那麼就會無法使用 Cookie 來儲存,屆時就只能使用 localStorage 來儲存,只是就必須多加注意 localStorage 的安全性問題

第四個結論:使用者的操作

基本上如果都已經盡可能遵守以上,並且系統也有做好安全性更新檢查的話,剩下的部分就會跟使用者比較有關,如果是因為使用者的操作不慎而導致資料外洩的話,那麼就只能說是使用者的問題了,這就不會是開發者的問題。

例如:使用者在公共場所登入,但是沒有登出或是使用者在公共場所登入,但是沒有關閉瀏覽器等等,這些都是屬於使用者本身的問題,這種就比較難以解決,雖然我們可以設計自動登出的功能,但是百密一疏,還是有可能會因為使用者操作不當而外洩 Token。

結尾

雖然使用 localStorage 很方便,但是考慮到一些可能的安全性問題,因此建議還是使用 Cookie 來儲存 Token,並且設置有效期限,這樣就可以盡可能的去避免一些可能的資安問題,所以我將瀏覽器所提供的那幾個儲存空間定義如下

  • localStorage:半永久的儲存空間,適合儲存一些不會過期的資料,空間雖然有 5MB,但是要注意安全性的問題以及小心 XSS 的問題
  • sessionStorage:短暫的儲存空間,適合儲存一些不重要,頁面關閉就會消失的資料,同上
  • Cookie:短暫的儲存空間,適合儲存一些會過期的資料,例如:Token 等敏感資訊,但要注意空間僅有 4KB 的問題,以及小心 CSRF 的問題

所以其實不論是那一種儲存空間,都有其適合的使用場景,只是要注意安全性的問題,並且遵守一些原則,這樣就可以盡可能的避免一些可能的資安問題哩

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ