筆記 - JWT 認證機制與實作流程
之前在 AC 課程最後一週才學到前後分離與 JWT 認證機制,當時進度很趕。趁做另一個專案的時候來複習,並記錄開發流程。
Intro
HTTP stateless 的特性下,每個 request 都是獨立的。故若想保留使用者登入狀態,常用「交換憑證」的方式來達成。
cookie-based 登入機制
- cookie:用戶端的憑證,有如會員卡,卡上有會員編號 (也就是 session_id)
- session:伺服器端的憑證對照表,有如會員名冊,可以透過會員編號 (session_id) 查找到使用者資訊 (user_id)
當 client 端發送請求時,會在 cookie 中帶有 session_id 一起傳送給伺服器,伺服器到 session 中尋找,若成功找到該 id 即可確認使用者身份。
但是cookie 的值只能在特定網域內被存取,在前後分離的開發中,當前後端站部署在不同網域時,就會無法使用 cookie-based 的方式去做身份驗證。
token-based 登入機制
與 cookie-based 同樣是「交換憑證讓 client 與 server 認出彼此」的機制,但改用 token 來做為憑證。client 端在 POST /singin
的時候,會用帳號與密碼向 server 拿到一個 token ,之後發送的每一個 HTTP request 都夾帶這個 token。
JWT: JSON Web Token
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
用 JSON object 來製作 token,規範了 token 由三個部分所組成:
- Header - 標記 token 的類型與雜湊函式名稱
- Payload - 要攜帶的資料,例如 user_id 與時間戳記,也可以指定 token 的過期時間
- Signature - 根據 Header 和 Payload,加上密鑰 (secret) 進行雜湊,產生一組不可反解的亂數,當成簽章,用來驗證 JWT 是否經過篡改。
實作
登入機制簡單分為兩個階段
(1) 登入 => 簽發 JWT
client: POST/singin
with account and password
server: validate account and password => find the user => sign a JWT => send back to client
(2) 身份認證 => 使用網站服務
client: send request bearer JWT
server: verify JWT and find user thought password.authenticate()
=> send req.user => accept the request
登入與簽發 JWT
在 router 檔案中新增登入路由,注意登入路由不需要認證
1 | router.post('/users/signin', userController.signIn) |
在 controller 中引入 jwt 套件,並簽發 JWT:
jwt.sign(payload, secretOrPrivateKey, [options, callback])
payload
: 想要打包的資訊 (object)secretOrPrivateKey
: 專案是使用 secret,JWT 會拿 secret 加上 header 和 payload 進行雜湊產生一組不可反解的亂數,避免 payload 和 header 資訊被篡改。
(Asynchronous) If a callback is supplied, the callback is called with the err or the JWT.
(Synchronous) Returns the JsonWebToken as string
1 | // controller/user-controller.js |
身份認證
client 發出請求時同時攜帶 token
server 需負責解析 token 是否合法且有效。
實作包含兩階段:(1) 設定 passport-jwt strategy (2) 實作 middleware 並視需求加入路由中。
設定 Passport-jwt strategy (config/passport.js)
npm i passport passport-jwt
touch config/passport.js
在這邊使用 passport.use() 設定所要使用的 authentication strategy,寫法為 new JwtStrategy(options, verify)
,參考 passport-jwt 文件
options
: 會根據 strategy 不同有各種不同客制選項,以下為 jwt 的
- secretOrKey: 密鑰 (REQUIRED unless secretOrKeyProvider is provided)
- jwtFromRequest (REQUIRED): Request 如何攜帶 JWT 的方法,參考 設定選項
- passReqToCallback: If true the request will be passed to the verify callback. i.e.
verify(request, jwt_payload, done_callback)
. 設定 true 可以把 callback 的第一個參數拿到 req 裡(在 local strategy 的時候需要req.flash
的時候會有用,要設定 true) - 其他例如像加密演算法、或是要指定 issuer, audience (token 簽發者與對象?)等等
verify(jwt_payload, done)
: 驗證函式
- jwt_payload: an object literal containing the decoded JWT payload.
- done: a passport error first callback accepting arguments done(error, user, info)
1 | // config/passport.js |
將認證程序封裝成 middleware 加入路由中
touch middleware/auth.js
1 | // middleware/auth.js |
參考 express 官方文件 我們知道 app.METHOD(path, callback, [, callback ...])
其中 callback 可以是 middleware (用逗號連、或是放在 array 裡面、或是前述組合等等)。
別忘了在主程式 app.js 中引入 passport 套件,並且初始化
1 | // 主程式 app.js |
然後就可以將認證 middleware 放在路由中了
1 | // routes/index.js |
Summary
- 登入 controller 中引入 jsonwebtoken 套件,使用
jwt.sign()
簽發 token (參數: payload 是什麼內容、secret 、token 效期等) config/passport.js
檔案中設定 authentication strategy,不同的 strategy 有不同的客制選項(jwt options: secret, token 夾帶方式等)。passport.use(new JwtStrategy(jwtOptions, function(payload, next){...})
,本次專案用user.id
作為payload.id
,故User.findByPk()
傳入payload.id
,找到 user 的話就回傳- 引入設定好的 passport 檔案(
config/passport.js
) 使用passport.authenticate()
來進行認證程序,並將其封裝成 middleware,放在路由中。也別忘了在主程式中引入 passport 並在進入路由前初始化app.use(passport.initialize())
。