1. 程式人生 > 實用技巧 >身份認證 ( Session、JWT )

身份認證 ( Session、JWT )

身份認證(Authentication)又稱“身份驗證”、“鑑權”,是指通過一定的手段,完成對使用者身份的確認。

認證 / 授權

認證(Authentication):驗證目標物件身份。比如,通過使用者名稱和密碼登入某個系統就是認證。

授權(Authorization):給予通過驗證的目標物件操作許可權。

不同開發模式下的身份認證 對於服務端渲染和前後端分離這兩種開發模式來說,分別有著不同的身份認證方案: ① 服務端渲染推薦使用 Session 認證機制 ② 前後端分離推薦使用 JWT 認證機制

JWT應用場景:一次性驗證

Session 認證機制
1. HTTP 協議的無狀態性 HTTP 協議的無狀態性,指的是客戶端的每次 HTTP 請求都是獨立的,連續多個請求之間沒有直接的關係,伺服器不會 主動保留每次 HTTP 請求的狀態。 2. Cookie Cookie 是儲存在使用者瀏覽器中的一段不超過 4 KB 的字串。它由一個名稱(Name)、一個值(Value)和其它幾個用 於控制 Cookie 有效期、安全性、使用範圍的可選屬性組成。 不同域名下的 Cookie 各自獨立,每當客戶端發起請求時,會自動把當前域名下所有未過期的 Cookie 一同傳送到伺服器。 Cookie的幾大特性: ① 自動傳送 ② 域名獨立 ③ 過期時限 ④ 4KB 限制
3. Cookie 在身份認證中的作用 客戶端第一次請求伺服器的時候,伺服器通過響應頭的形式,向客戶端傳送一個身份認證的 Cookie,客戶端會自動 將 Cookie 儲存在瀏覽器中。 隨後,當客戶端瀏覽器每次請求伺服器的時候,瀏覽器會自動將身份認證相關的 Cookie,通過請求頭的形式傳送給 伺服器,伺服器即可驗明客戶端的身份。 4. Cookie 不具有安全性 由於 Cookie 是儲存在瀏覽器中的,而且瀏覽器也提供了讀寫 Cookie 的 API,因此 Cookie 很容易被偽造,不具有安全 性。因此不建議伺服器將重要的隱私資料,通過 Cookie 的形式傳送給瀏覽器。 Cookie儲存在瀏覽器中,很容易被偽造,不具有安全性.
千萬不要使用 Cookie 儲存重要且隱私的資料!比如使用者的身份資訊、密碼等 6. 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: *

JWT 認證機制( Json web token) JWT(英文全稱:JSON Web Token)是目前最流行的跨域認證解決方案 工作原理: 使用者的資訊通過 Token 字串的形式,儲存在客戶端瀏覽器中。伺服器通過還原 Token 字串的形式來認證使用者的身份。 JWT 通常由三部分組成,分別是 Header(頭部)、Payload(有效荷載)、Signature(簽名)。 三者之間使用英文的“.”分隔 格式: JWT 字串的示例: JWT 的三個部分各自代表的含義 JWT 的三個組成部分,從前到後分別是 Header、Payload、Signature。 其中: ⚫ Payload 部分才是真正的使用者資訊,它是使用者資訊經過加密之後生成的字串。 ⚫ Header 和 Signature 是安全性相關的部分,只是為了保證 Token 的安全性

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 的特性天然不支援續簽