身份認證 ( Session、JWT )
認證 / 授權
認證(Authentication):驗證目標物件身份。比如,通過使用者名稱和密碼登入某個系統就是認證。
授權(Authorization):給予通過驗證的目標物件操作許可權。
不同開發模式下的身份認證 對於服務端渲染和前後端分離這兩種開發模式來說,分別有著不同的身份認證方案: ① 服務端渲染推薦使用 Session 認證機制 ② 前後端分離推薦使用 JWT 認證機制JWT應用場景:一次性驗證
Session 認證機制首先客戶端發起請求,服務端驗證請求的合法性,把請求的資訊儲存到伺服器上,並以響應頭的形式向瀏覽器傳送cookie,用以儲存sessionId;
客戶端下次發起請求時自動攜帶著cookie中的sessionId來驗證身份,在伺服器上核對身份之後對請求作出響應。
session需要把id以cookie的形式放在客戶端,所以session依賴於cookie
Express使用session
首先安裝express-session中介軟體:npm install express-session
// 1.配置 Session 中介軟體
const session = require('express-session');
app.use(session({
//secret屬性的值可以為任意字串
secret: 'keyboard',
//固定寫法
resave: false,
//固定寫法
saveUninitialized: true,
})
)
// 儲存session資訊
req.session.user = req.body // 使用者的資訊
// 讀取session資訊
req.session.user.username
// 清空 Session 資訊
req.session.destroy()
Session 認證的侷限性
Session 認證機制需要配合 Cookie 才能實現。由於 Cookie 預設不支援跨域訪問,所以,當涉及到前端跨域請求後端接
口的時候,需要做很多額外的配置,才能實現跨域 Session 認證。
⚫ 當前端請求後端介面不存在跨域問題的時候,推薦使用 Session 身份認證機制。
⚫ 當前端需要跨域請求後端介面的時候,不推薦使用 Session 身份認證機制,推薦使用 JWT 認證機制。
session認證所顯露的問題
Session: 每個使用者經過我們的應用認證之後,我們的應用都要在服務端做一次記錄,以方便使用者下次請求的鑑別,
通常而言session都是儲存在記憶體中,而隨著認證使用者的增多,服務端的開銷會明顯增大。
擴充套件性: 使用者認證之後,服務端做認證記錄,如果認證的記錄被儲存在記憶體中的話,這意味著使用者下次請求還必須要請求在這臺伺服器上,
這樣才能拿到授權的資源,這樣在分散式的應用上,相應的限制了負載均衡器的能力。這也意味著限制了應用的擴充套件能力。
因為是基於cookie來進行使用者識別的, cookie如果被截獲,使用者就會很容易受到跨站請求偽造的攻擊。
基於token的鑑權機制
基於token的鑑權機制類似於http協議也是無狀態的,它不需要在服務端去保留使用者的認證資訊或者會話資訊
。這就意味著基於token認證機制的應用不需要去考慮使用者在哪一臺伺服器登入了,這就為應用的擴充套件提供了便利。
- 使用者使用使用者名稱密碼來請求伺服器
- 伺服器進行驗證使用者的資訊
- 伺服器通過驗證傳送給使用者一個token
- 客戶端儲存token,並在每次請求時附送上這個token值
- 服務端驗證token值,並返回資料
這個token必須要在每次請求時傳遞給服務端,它應該儲存在請求頭裡,
另外,服務端要支援CORS(跨來源資源共享)
策略,一般我們在服務端這麼做就可以了
Access-Control-Allow-Origin: *
header
jwt的頭部承載兩部分資訊:
- 宣告型別,這裡是jwt
- 宣告加密的演算法 通常直接使用 HMAC SHA256
- 完整的頭部就像下面這樣的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
載荷就是存放有效資訊的地方。這個名字像是特指飛機上承載的貨品,這些有效資訊包含三個部分
- 標準中註冊的宣告
- 公共的宣告
- 私有的宣告
標準中註冊的宣告 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的使用者
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什麼時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的宣告 :
公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊.但不建議新增敏感資訊,因為該部分在客戶端可解密.
私有的宣告 :
私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,意味著該部分資訊可以歸類為明文資訊。
定義一個payload:
{
"sub": "123456789",
"name": "eee",
"admin": true
}
然後將其進行base64加密,得到Jwt的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
jwt的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
- header (base64後的)
- payload (base64後的)
- secret
這個部分需要base64加密後的header和base64加密後的payload使用.
連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.
連線成一個完整的字串,構成了最終的jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是儲存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證所以,
它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
JWT 的使用方式
客戶端收到伺服器返回的 JWT 之後,通常會將它儲存在 localStorage 或 sessionStorage 中。
此後,客戶端每次與伺服器通訊,都要帶上這個 JWT 的字串,從而進行身份認證。推薦的做法是把 JWT 放在 HTTP
請求頭的 Authorization 欄位中,
格式:
Authorization:bearer <token>
在 Express 中使用 JWT
1. 安裝 JWT 相關的包
執行如下命令,安裝如下兩個 JWT 相關的包:
npm install jsonwebtoken express-jwt
其中:
⚫ jsonwebtoken 用於生成 JWT 字串
⚫ express-jwt 用於將 JWT 字串解析還原成 JSON 物件
2. 匯入 JWT 相關的包
使用 require() 函式,分別匯入 JWT 相關的兩個包:
分別是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
例:
// 匯入 express 模組
const express = require('express')
// 建立 express 的伺服器例項
const app = express()
//1:安裝並匯入 JWT 相關的兩個包,分別是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 允許跨域資源共享
const cors = require('cors')
app.use(cors())
// 解析 post 表單資料的中介軟體
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
//2:定義 secret 金鑰,建議將金鑰命名為 secretKey
const secretKey = 'itheima No1 ^_^'
// 4:註冊將 JWT 字串解析還原成 JSON 物件的中介軟體
// 注意:只要配置成功了 express-jwt 這個中介軟體,就可以把解析出來的使用者資訊,掛載到 req.user 屬性上
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))
// 登入介面
app.post('/api/login', function (req, res) {
// 將 req.body 請求體中的資料,轉存為 userinfo 常量
const userinfo = req.body
// 登入失敗
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登入失敗!',
})
}
// 登入成功
// 3:在登入成功之後,呼叫 jwt.sign() 方法生成 JWT 字串。並通過 token 屬性發送給客戶端
// 引數1:使用者的資訊物件
// 引數2:加密的祕鑰
// 引數3:配置物件,可以配置當前 token 的有效期
// 記住:千萬不要把密碼加密到 token 字元中
const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登入成功!',
token: tokenStr, // 要傳送給客戶端的 token 字串
})
})
// 這是一個有許可權的 API 介面
app.get('/admin/getinfo', function (req, res) {
// 5:使用 req.user 獲取使用者資訊,並使用 data 屬性將使用者資訊傳送給客戶端
console.log(req.user)
res.send({
status: 200,
message: '獲取使用者資訊成功!',
data: req.user, // 要傳送給客戶端的使用者資訊
})
})
//6:使用全域性錯誤處理中介軟體,捕獲解析 JWT 失敗後產生的錯誤
app.use((err, req, res, next) => {
// 這次錯誤是由 token 解析失敗導致的
if (err.name === 'UnauthorizedError') {
return res.send({
status: 401,
message: '無效的token',
})
}
res.send({
status: 500,
message: '未知的錯誤',
})
})
// 呼叫 app.listen 方法,指定埠號並啟動web伺服器
app.listen(8888, function () {
console.log('Express server running at http://127.0.0.1:8888')
})
優點
- 因為json的通用性,所以JWT是可以進行跨語言支援的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
- 因為有了payload部分,所以JWT可以在自身儲存一些其他業務邏輯所必要的非敏感資訊。
- 便於傳輸,jwt的構成非常簡單,位元組佔用很小,所以它是非常便於傳輸的。
- 它不需要在服務端儲存會話資訊, 所以它易於應用的擴充套件
安全相關
- 不應該在jwt的payload部分存放敏感資訊,因為該部分是客戶端可解密的部分。
- 保護好secret私鑰,該私鑰非常重要。
- 如果可以,請使用https協議
無法滿足登出場景
傳統的 session+cookie 方案使用者點選登出,服務端清空 session 即可,因為狀態儲存在服務端。
但 jwt 的方案就比較難辦了,因為 jwt 是無狀態的,服務端通過計算來校驗有效性。
沒有儲存起來,所以即使客戶端刪除了 jwt,但是該 jwt 還是在有效期內,只不過處於一個遊離狀態。
無法滿足修改密碼場景
修改密碼則略微有些不同,假設號被到了,修改密碼(是使用者密碼,不是 jwt 的 secret)之後,盜號者在原 jwt 有效期之內依舊可以繼續訪問系統,
所以僅僅清空 cookie 自然是不夠的,這時,需要強制性的修改 secret。
無法滿足token續簽場景
我們知道微信只要你每天使用是不需要重新登入的,因為有token續簽,因為傳統的 cookie 續簽方案一般都是框架自帶的,
session 有效期 30 分鐘,30 分鐘內如果有訪問,session 有效期被重新整理至 30 分鐘。
但是 jwt 本身的 payload 之中也有一個 exp 過期時間引數,來代表一個 jwt 的時效性,
而 jwt 想延期這個 exp 就有點身不由己了,因為 payload 是參與簽名的,一旦過期時間被修改,整個 jwt 串就變了,jwt 的特性天然不支援續簽