JWT 介紹

JWT 運作原理

當使用者傳送post request 到伺服器時,伺服器會產生Json Web Token用來加密使用者傳來的資料,並使用 secrest key來加密。
加密完後就會將JWT回傳給瀏覽器,瀏覽器可以選擇各種儲存token的方式例如cookie。
當使用者再次發出request請求時,會在header附帶上JWT token,伺服器收到token後會使用 同樣secrest key來進行解密,確認token的資料是否一樣。確認token沒問題後,就可從token中取得user的資料,例如帳密,再回傳給前端。

和session的差異

session運作的原理和jwt類似,但主要差別是,session會儲存user資料在伺服器,伺服器利用session id來尋找user資料。
jwt則將使用者資料存在token裡,token則會被保存在前端,伺服器不用儲存任何資料。也就是說,可以使用同樣的token在不同的伺服器上,不像session可能遇到不同伺服器有不同session而無法登入的情況。

JWT 加密過程

左欄是加密後的JWT TOKEN,右欄則是解密後的token,解密後的token分為三部分:

  • HEADER: 包含用來加密、解密的演算法。
  • PAYLOAD: 儲存在token裡的資料。
  • VERIFY SIGNATURE: 用來驗證token是否被使用者更改。

使用base64URL來加密或解密HEADER跟PAYLOAD,並使用secret Key來加解密,也就是左欄紅色跟紫色的密碼。
實際流程: 當伺服器收到jwt token後,會將左欄紅色跟紫色的密碼,用指定的演算法(上圖是HS256)來hash,並確認hash後的值是否等於最後一段藍色的密碼。

實戰加密流程

安裝套件

在node.js專案中,先下載套件 jsonwebtoken

1
npm install --save jsonwebtoken 

在 controller 裡引入jwt

1
const jwt = require('jsonwebtoken');

後端加密

1
2
3
4
5
6
7
8
9
const login = async (req, res) => {
const { username, password } = req.body;
const id = new Date().getDate();
const token = jwt.sign({ id, username }, process.env.JWT_SECRET, { expiresIn: '30d' })
res.status(200).json({
msg: 'user created',
token,
});
}

使用 jwt.sign() 來加密,加密完後將token回傳給前端。

1
jwt.sign({ payload }, 金鑰, { expiresIn : 過期時間} )
  • 第一個參數為 payload: 使用者和相關的資訊都可以放置其中(ex: id、name)。
  • 第二個是金鑰字串,通常會是複雜的字串存在.env檔裡
  • 第三個可傳入 token 過期時間

不要將隱私資訊存放在 Payload 當中, Payload 和 Header 被轉換成 Base64 編碼後,能夠被輕易的轉換回來
因此不應該把如用戶密碼等重要資料存取在 Payload 當中

前端接收token

前端發出網路請求後成功後(例如登入成功),會收到後端回傳的token,前端可以先存入 localStorage

1
2
const { data } = await axios.post('/api/v1/login', { username, password })
localStorage.setItem('token', data.token)

當再次發出的請求是需要驗證時,需要傳token給後端驗證。在HEADER中使用Authorization並帶入存取的Token

1
2
3
4
5
6
const token = localStorage.getItem('token')
const { data } = await axios.get('/api/v1/dashboard', {
headers: {
Authorization: `Bearer ${token}`,
},
})

後端接收token

當後端收到前端帶有token的請求後,可以從 req.headers.authorization 裡收到token的值。

1
2
3
{
authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTgsInVzZXJuYW1lIjoicGV0ZXIiLCJpYXQiOjE2NTgxMjk2OTQsImV4cCI6MTY2MDcyMTY5NH0.4RNbP5jIx9GU1-makuVaxhU3g-cSbXhRJWynitjhu3Y',
}

用split方法取得 Bearer 後面的token字串

1
2
3
4
5
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new CustomAPIError('No token provided', 400);
}
const token = authHeader.split(' ')[1];

取得 token字串後,可以用 jwt.verify(token, 金鑰) 來解析

1
const decoded = jwt.verify(token, process.env.JWT_SECRET);

當token解析符合金鑰字串時,就會將當初加密的物件(payload)解析出來:

1
2
3
4
{
id: '123',
name: 'tim'
}