首頁 > 軟體

JS前端認證授權技巧歸納總結

2023-03-27 06:01:00

概念介紹

認證授權在業界已經有很多成熟的方案,但對於前端開發來說,大部分情況都是呼叫伺服器端提供登入介面完成認證,後續請求帶上對於 token 即可。相信不少公司還會開發 JS SDK,讓各個業務專案都能快速接入認證授權,所以認證授權的細節流程我們可能並沒有關注過。

本文將從前端視角介紹常見的認證授權方案,同時提供簡單的實踐來體驗流程。

認證

認證 (Identification) 是驗證當前使用者的身份。

常見的認證技術:

  • 身份證
  • 使用者名稱和密碼
  • 使用者手機:手機簡訊、手機二維條碼掃描、手勢密碼
  • 使用者的電子郵箱
  • 使用者的生物學特徵:指紋、語音、眼睛虹膜

授權

授權 (Authorization) 指賦予使用者系統的存取許可權。認證完使用者身份後,系統會授予使用者部分或者全部許可權。系統要是沒有許可權控制需求的話,一般認證後用戶就有全部許可權。

實現授權的方式有:

  • cookie
  • session
  • token
  • OAuth

鑑權

鑑權 (Authentication) 是指系統鑑定使用者身份和許可權。比如系統需要鑑定 session/cookie/token 的合法性和有效性。

認證、授權和鑑權關係

這三個概念的關係也是很清晰,就是一個前後依次發生的關係:認證 => 授權 => 鑑權。比如我們登入某個系統就完成了認證和授權,後續使用功能時就需要系統鑑權。

瞭解相關概念後,就可以開始介紹常見的認證授權方案。

認證授權方案

HTTP 基本認證

基本認證 (Basic 認證) 是 HTTP/1.0 就定義的認證方式,主要通過使用者提供使用者名稱和密碼的方式,實現對使用者身份的驗證。

基本認證流程圖

認證步驟解析

  • 瀏覽器請求受保護的資源
  • 伺服器返回 401,同時響應頭帶上 WWW-Authenticate: Basic realm=protected_docs,realm 代表資源的安全域,我們資源可能許可權不同,放在不同的安全域中
  • 瀏覽器彈窗請求使用者賬號密碼,使用者輸入後,瀏覽器 Base64 編碼賬號密碼,再次請求,請求頭帶上 Authorization: Basic Base64(賬號:密碼)
  • 伺服器認證賬號密碼,認證成功返回對應資源,認證失敗返回 403 forbidden

node 的簡單實現

const express = require('express')
const app = express()
const protectedPath = '/protected_docs'
const realms = {
  [protectedPath]: {
    users: ['root'],
  },
}
app.get(protectedPath, (req, res, next) => {
  const realm = realms[req.path]
  const authorization = req.get('authorization')
  if (!authorization) {
    // 告知使用者需要身份認證
    res.statusCode = 401
    res.set('WWW-Authenticate', 'Basic realm=' + encodeURIComponent(realm))
    res.end()
    return
  }
  const usernamePasswd = authorization.split(' ')[1] // Basic Y2h5aW5ncDoxMjM0NTY
  const [usrname, passwd] = Buffer.from(usernamePasswd, 'base64')
    .toString()
    .split(':')
  if (!realm.users.includes(usrname)) {
    // 使用者不在realm裡
    res.statusCode = 401
    res.set('WWW-Authenticate', 'Basic realm=' + encodeURIComponent(realm))
    res.end()
    return
  }
  const isValid = usrname === 'root' && passwd === '123456'
  if (!isValid) {
    // 使用者賬號、密碼驗證不通過
    res.statusCode = 403
    res.end()
    return
  }
  res.end(`welecom ${usrname}`)
})
app.listen(3000)

小結

優點:

  • 簡單,基本所有瀏覽器都支援

缺點:

  • 不安全,HTTP 上傳輸,密碼只是 Base64 編碼,可以被解碼。
  • 無法主動登出,除非分頁或瀏覽器關閉、或使用者清除歷史記錄,認證資訊一直存在。

使用場景:

  • 內部網路,或者對安全要求不是很高的網路。比如公司內網 wiki

這邊提一下,HTTP1.1 針對基本認證缺點,提供了摘要認證(Digest 認證),原理簡單來說就是伺服器端會給瀏覽器一個 nonce 亂數,瀏覽器會將賬號、密碼和 nonce 等引數進行 md5 加密後傳給伺服器端(同時傳賬號資料,密碼不傳),伺服器端獲取到賬號後,從資料庫拿密碼同樣進行 md5 加密,加密後值和瀏覽器傳的一樣就認為認證成功。

摘要認證不再明文傳輸密碼、可以防重放和避免報文被篡改,但是需要和 Https 配合使用。

Session-Cookie

Session-Cookie 認證是利用伺服器端的 Session(對談)和瀏覽器(使用者端)的 Cookie 來實現的前後端通訊認證模式。

什麼是 Cookie

HTTP 是無狀態協定,伺服器端在接收到使用者端首次請求後,設定對應的 Cookie,隨後瀏覽器在請求帶上 Cookie,伺服器端就可以知道當前使用者端狀態。

Cookie 的特點:

  • Cookie 儲存在使用者端,可能被修改,不安全
  • 有大小限制,一般是 4KB
  • Android 和 IOS 對 Cookie 支援不好
  • Cookie 有跨域限制

什麼是 Session

Session 的抽象概念是對談,是無狀態協定通訊過程中,為了實現中斷/繼續操作,將使用者和伺服器之間的互動進行的一種抽象。

Session 一般流程是:伺服器端接收到使用者端首次請求後,設定一個 Session 來跟蹤使用者的對談,同時會給使用者端一個 Session ID,後續使用者端請求時在帶上 Session ID,伺服器端即可找到對應的 Session,此時雙方通訊就是有狀態的。

Session 特點:

  • Session 資料儲存在伺服器端,安全性高
  • Session 資料大小可以超過 4KB,儲存數

Session-Cookie 流程

Session 流程中一般會設定 Session ID,通常 Session ID 會儲存在 瀏覽器 Cookie 中,接下來看下整體流程。

認證步驟解析

  • 瀏覽器傳送登入請求
  • 伺服器端校驗賬號密碼,校驗通過後生成 Session 和 Session ID,Session 儲存在 Session 伺服器中(一般儲存在記憶體或者 Redis 伺服器)。隨後返回資料給瀏覽器,同時設定一個 Session ID 的 Cookie
  • 瀏覽器請求資源,一般會自動帶上 Session ID 的 Cookie
  • 伺服器端獲取到 Session ID,通過 Session 伺服器校驗 Session,Session 伺服器校驗成功,伺服器端處理資料邏輯
  • 伺服器端返回資料給瀏覽器

簡單程式碼範例

const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const port = 3000;
const session = require("express-session");
app.use(bodyParser());
app.use(
  session({
    key: 'SESSION_ID',
    secret: "your_secret_key",
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 1000 * 60 * 60 * 8, signed: true },
  })
);
app.get("/", async function (req, res) {
  res.send(req.session);
});
app.get("/login", async function (req, res) {
  const authInfo = {
    id: '1',
    username: 'user',
  }
  const isValid = true
  if (isValid) {
    req.session.authInfo = authInfo
    res.send({
      success: true,
      info: "登入成功",
    });
  } else {
    res.send({
      success: false,
      info: "登入失敗",
    });
  }
});
app.listen(port, () => {
  console.log(`node listening at http://localhost:${port}`);
});

使用 node 起好伺服器後,先存取 /login,在存取首頁 /, 可以看到首頁輸出使用者名稱。同時開啟 F12,在 Cookie 中可以看到 SESSION_ID 的資料。

小結

優點:

  • Cookie 簡單易用
  • Session 資料儲存在伺服器端,安全
  • 支援 Session 管理,能主動登出 Session
  • 只需後端操作,前端無感

缺點:

  • 依賴 Cookie,禁用 Cookie 情況下無法使用
  • 因為使用了 Cookie ,所以可能有 CSRF 攻擊
  • Session 存在伺服器端,使用者量大的情況下,伺服器端開銷增加,效能會下降
  • 對行動端的支援性不友好

使用場景:

  • 一般中大型的網站都適用

Token

上述介紹中,我們知道了 Session-Cookie 的一些缺點,及 Session 的維護給伺服器端造成很大困擾,必須找地方存放它,又要考慮分散式的問題,所以 Token 方案就出來了。

什麼是 Token

Token 是一個令牌,使用者端存取伺服器時,驗證通過後伺服器端會為其簽發一張令牌,之後,使用者端就可以攜帶令牌存取伺服器,伺服器端只需要驗證令牌的有效性即可。

一般 Token 的組成:

uid(使用者唯一的身份標識) + time(當前時間的時間戳) + sign(簽名,Token 的前幾位以雜湊演演算法壓縮成的一定長度的十六進位制字串)

Token 認證流程

認證步驟解析

  • 使用者端傳送登入請求
  • 伺服器端校驗賬號密碼,生成 Token,並返回給使用者端
  • 收到 Token 以後需要把它儲存起來,web 端一般會放在 localStorage 或 Cookie 中
  • 使用者端請求 API 資源的時候,將 Token 通過 HTTP 請求頭 Authorization 欄位或者其它方式傳送給伺服器端
  • 伺服器端拿到 Token,做解密和簽名校驗,通過校驗返回資料,否則返回 401

Token的優缺點

優點:

  • 伺服器端無狀態化、可延伸性好:Token 自身包含了其所標識使用者的相關資訊,這有利於在多個服務間共用使用者狀態
  • 安全性好,可以避免 CSRF 攻擊
  • 支援跨域呼叫

缺點:

  • 效能問題,伺服器端需要對 Token 加解密等操作,所以會更耗效能
  • 有效期短:為了避免 Token 被盜用,一般 Token 的有效期會設定的較短,所以就有了 Refresh Token
  • 相比於 Session-Cookie,需要前後端配合處理

Refresh Token

業務介面用來鑑權的 Token,我們稱之為 Access Token,為了安全性,Access Token 有效期一般設定的比較短。Access Token 過期後,需要使用者重新登入,但是這種體驗較差。

所以有了 Refresh Token, 可以用 Refresh Token 去獲取 Access Token。

  • Access Token:用來存取業務介面,由於有效期足夠短,盜用風險小。

  • Refresh Token:用來獲取 Access Token,有效期可以長一些,通過獨立服務和嚴格的請求方式增加安全性。

Refresh Token 的使用流程是在伺服器校驗 Token, 發現過期後,使用者端可以使用 Refresh Token 發起請求,獲取新的 Access Token 和 Refresh Token。

JSON Web Token(JWT)

上述 Token 中,一般只有 uid 資訊,需要更多登入資訊和其他資料的話,這時就需要查詢資料庫。每次都需要查詢資料庫,就會帶來一些效能消耗。所以業界常用的 JWT 方案就出來了。

JWT 是 Auth0 提出的通過對 JSON 進行加密簽名來實現授權驗證的方案, 它的特點是自包含的,使用者資訊和認證是在一起的,無需像 Cookie-Session 一樣需要 Session 伺服器,或者像 Token 一樣存取資料庫獲取使用者資訊。

JWT 本質上就是一組字串,通過(.)切分成三個為 Base64 編碼的部分:

  • Header : 描述 JWT 的後設資料,定義了生成簽名的演演算法以及 Token 的型別。
  • Payload : 用來存放實際需要傳遞的資料,JWT 規定了 7 個官方欄位,比如 iss、exp 等等,還可以自定義資料。
  • Signature(簽名):伺服器通過 Payload、Header 和一個金鑰 (Secret) 使用 Header 裡面指定的簽名演演算法(預設是 HMAC SHA256)生成。

JWT 通常是這樣的:xxxxx.yyyyy.zzzzz。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJuYW1lIjoieGlhb21pbmciLCJkYXRhIjoiPT09PT09PT09PT09PSIsImlhdCI6MTY3OTgwNDA1NywiZXhwIjoxNjc5ODA0MTE3fQ
.FdJ6UD4Ff5zOz83f4hRDh1C86kN5f8aO_KeEtIwt3cM

JWT 認證流程

其實 JWT 的認證流程與 Token 的認證流程差不多,只是不需要再單獨去查詢資料庫查詢使用者資訊。

JWT 範例

const { expressjwt: jwt } = require('express-jwt')
const express = require('express')
const app = express()
const jsonwebtoken = require('jsonwebtoken')
const secretOrPrivateKey = 'hello' //加密token 校驗token時要使用
app.use(
  jwt({
    secret: secretOrPrivateKey,
    algorithms: ['HS256'],
  }).unless({
    path: ['/getToken'], //除了這個地址,其他的URL都需要驗證
  })
)
app.use(function (err, req, res, next) {
  if (err.name === 'UnauthorizedError') {
    res.status(401).send('invalid token...')
  }
})
app.get('/getToken', function (req, res) {
  res.json({
    result: 'ok',
    token: jsonwebtoken.sign(
      {
        name: 'xiaoming',
        data: '=============',
      },
      secretOrPrivateKey,
      {
        expiresIn: 60 * 1,
      }
    ),
  })
})
app.get('/getData', function (req, res) {
  res.send('data')
})
app.listen(3000)

服務啟動後,存取 /getToken 獲取 JWT,然後在 Postman 中請求 /getData, Header 部分加上 Authorization: Bearer your jwt,比如 Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoieGlhb21pbmciLCJkYXRhIjoiPT09PT09PT09PT09PSIsImlhdCI6MTY3OTgwNjkyOCwiZXhwIjoxNjc5ODA2OTg4fQ.qFOT9IS_T1ZNsWWheRXP9MxYPh1l3SBGWLtp8ocnKAE

JWT 的優缺點

優點:

  • 不需要在伺服器端儲存對談資訊,所以易於應用的擴充套件
  • JWT 中的 Payload 負載可以儲存常用資訊,用於資訊交換,有效地使用 JWT,可以降低伺服器端查詢資料庫的次數

缺點:

  • 請求頭體積較大,Payload 資料量大的時候,請求頭體積也會對應增大
  • JWT 預設是不加密,所以 Payload 一般不放敏感資訊,不過也可以再次加密。
  • 到期問題,JWT 一旦簽發後,除非到了過期時間,不然會一直有效,伺服器端無法主動登出掉。

使用場景:

  • 一般對安全要求不高,對負載要求高的場景都會可以使用 JWT,安全敏感的場景推薦使用 token + redis
  • 單點登入

Oauth 2.0

OAuth 這種方式登入相信大家都使用過,比如我們想登入某個網站時,通常會發現可以通過第三方的 QQ 或者微信登入,這個就是使用到了 OAuth。

OAuth 是一個開放標準,允許使用者授權第三方網站存取他們儲存在另外的服務提供者上的資訊,而不需要將使用者名稱和密碼提供給第三方網站。常見的提供 OAuth 認證服務的廠商:支付寶、QQ、微信、微博

OAuth 認證流程

認證步驟解析

  • 使用者端發起使用第三方登入,伺服器端攜帶 client_id 直接重定向到授權伺服器登入頁面,授權伺服器要求使用者端登入。
  • 使用者端完成第三方登入並同意授權後,授權伺服器重定向到伺服器端,並攜帶授權碼 code,伺服器攜帶 client_id, client_secret, code 向授權伺服器請求令牌。
  • 授權伺服器通過認證,並返回令牌
  • 伺服器端用令牌向授權伺服器請求使用者基本資訊,授權伺服器認證令牌通過後返回資料
  • 伺服器端返回使用者資訊給使用者端

使用 github 登入範例

準備工作:

1、建立 OAuth App

2、填寫基本資訊

3、獲取 client_idclient_secret

程式碼範例

const express = require('express')
const app = express()
const querystring = require('querystring')
const axios = require('axios')
// GitHub登入引數設定;設定授權應用生成的Client ID和Client Secret
const config = {
  client_id: 'xxx',
  client_secret: 'xxxxxx'
}
// 登入介面
app.get('/github/login', function (req, res) {
  // 重定向到GitHub認證介面,並設定引數
  let path = 'https://github.com/login/oauth/authorize?client_id=' + config.client_id
  // 轉發到授權伺服器
  res.redirect(path)
})
// GitHub授權登入成功回撥,地址必須與GitHub設定的回撥地址一致
app.get('/passport/github/callback', async function (req, res) {
  console.log('callback...')
  // 伺服器認證成功,回撥帶回認證狀態code
  const code = req.query.code
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }
  // 申請令牌token
  let tokenRes = await axios.post('https://github.com/login/oauth/access_token', params)
  const access_token = querystring.parse(tokenRes.data).access_token
  // 根據token獲取使用者資訊
  userRes = await axios.get(`https://api.github.com/user`, {
    headers: {
      'Authorization': 'token ' + access_token
    }
  })
  // 渲染頁面
  res.end(`
    <h1>Hello ${userRes.data.login}</h1>
    <img src="${userRes.data.avatar_url}" alt="">
  `)
})
app.listen(7001, () => {
  console.log('listening port at 7001...')
})

服務啟動後,存取 /github/login,後續會跳轉 github 登入授權,完成後即可看到你的 github 使用者名稱和頭像。

總結

登入的認證授權方式有很多,上述講到的都是一些常見的方案,當前方案的具體細節實施各個專案方還是會有差異的。

除了上述的內容後,認證授權這塊還有單點登入(SSO)、掃描登入、手機號一鍵登入等等方式,後續有機會在講。

以上就是JS前端認證授權技巧歸納總結的詳細內容,更多關於JS前端認證授權的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com