[譯]你不須要 passport.js — node.js認證指南

🛑你不須要 passport.js — node.js認證指南 ✌️

簡介

諸如 Google Firebase,AWS Cognito 以及 Auth0 這樣的第三方認證服務愈來愈流行,相似於 passport.js 這樣的一站式解決方案也成爲了業界標準,可是一個廣泛狀況是,開發者們其實並不清楚完整的認證流程到底涉及那些部分。javascript

這一系列關於 node.js 認證的文章,旨在讓你搞清楚一些概念,好比 JSON Web Token (JWT)、社交帳號登陸 (OAuth2)、用戶模仿(一個管理員無需密碼便能做爲特定用戶登陸)。前端

固然,文末也給你準備好了一個完整的 node.js 認證流程的代碼庫,放在GitHub上了,你能夠做爲你本身項目的基礎來使用。java

前置知識 ✍️

在閱讀以前,你須要先了解:node

  • 數據庫中如何存儲用戶的 email 和密碼,或者客戶端ID和客戶端密鑰,或者其餘的密鑰對。
  • 至少一種健壯且高效的加密算法。

在我寫下這篇文章之時,我認爲 Argon2 是目前最好的加密算法,請不要用 SHA256,SHA512 或者 MD5 這類簡單的加密算法了。android

有關這點,有興趣的話能夠去看看這篇很是棒的文章 choosing a password hashing algorithm(如何選擇密碼哈希算法)ios

寫一個註冊程序 🥇

新用戶建立帳戶時,必須對密碼進行哈希處理並將其與電子郵件和其餘詳細信息(好比用戶配置文件、時間戳等等)一塊兒存儲在數據庫中。git

提示:你能夠去以前的文章瞭解 node.js 的項目結構 Bulletproof node.js project architecture 🛡️github

import * as argon2 from 'argon2';

class AuthService {
  public async SignUp(email, password, name): Promise<any> {
    const passwordHashed = await argon2.hash(password);

    const userRecord = await UserModel.create({
      password: passwordHashed,
      email,
      name,
    });
    return {
      // 絕對不要返回用戶的密碼!!!!
      user: {
        email: userRecord.email,
        name: userRecord.name,
      },
    }
  }
}
複製代碼

數據庫中,這名用戶的記錄看起來就是這樣:web

User record - Database MongoDB
Robo3T for MongoDB

再來寫一個登陸程序 🥈

Sign-In Diagram

當一名用戶想要登陸時,會發生下面的事情:算法

客戶端發送成對的公共標識(Public Identification)私鑰(Private key)

  • 服務端根據發來的 email 去數據庫查找用戶記錄。

  • 若是找到了,服務端會將收到的密碼進行哈希,而後和數據庫中已經哈希過的密碼進行比對。

  • 若是這兩個哈希值對上了,那麼服務端就發一個 JSON Web Token (JWT)。

這個 JWT 就是一個臨時 key,客戶端每次發器請求都須要帶上這個 Token

import * as argon2 from 'argon2';

class AuthService {
  public async Login(email, password): Promise<any> {
    const userRecord = await UserModel.findOne({ email });
    if (!userRecord) {
      throw new Error('User not found')
    } else {
      const correctPassword = await argon2.verify(userRecord.password, password);
      if (!correctPassword) {
        throw new Error('Incorrect password')
      }
    }

    return {
      user: {
        email: userRecord.email,
        name: userRecord.name,
      },
      token: this.generateJWT(userRecord),
    }
  }
}
複製代碼

這裏密碼認證使用了 argon2 庫來防止時序攻擊(timing-based attacks),也就是說,當攻擊者試圖靠蠻力破解口令時須要嚴格遵循服務器響應時間的相關準則。

接下來咱們將討論一下如何生成 JWT。

可是,JWT究竟是啥? 👩‍🏫

一個 JSON Web Token or JWT 是一個以字符串或者 Token 形式存儲的、通過編碼的 JSON 對象。

你能夠認爲它是 cookie 的替代者。

Token 有下面三個部分(不一樣顏色標註)

JSON Web Token example

JWT 中的數據能夠無需**密鑰(Secret)簽名(Signature)**在客戶端解碼。

所以對於用戶角色信息、配置文件、令牌過時時間等這些前端領域常見的信息或元數據(metadata)來講,編碼在 JWT 中一塊兒傳輸就變得很方便。

node.js 中如何建立 JWT? 🏭

咱們實現一個 generateToken 方法來完善咱們的認證服務程序吧。

經過使用 jsonwebtoken 這個庫(你能夠在 npmjs.com 找到它),咱們就能建立一個 JWT 了。

import * as jwt from 'jsonwebtoken'
class AuthService {
  private generateToken(user) {

    const data =  {
      _id: user._id,
      name: user.name,
      email: user.email
    };
    const signature = 'MySuP3R_z3kr3t';
    const expiration = '6h';

    return jwt.sign({ data, }, signature, { expiresIn: expiration });
  }
}
複製代碼

重要的是,永遠不要在編碼數據中包含用戶的敏感信息。

上面 signature 變量其實就是用來生成 JWT 的密鑰(secret),並且你要確保這個 signature 不會泄漏出去。

若是攻擊者經過某種方法獲取了 signature,他就能生成令牌而且假裝成用戶從而竊取他們的會話(session)。

保護端點以及驗證 JWT ⚔️

如今,前端須要在每一個請求中帶上 JWT 才能訪問到安全目標(secure endpoint)了。

一個比較好的作法是在請求的 header 中附帶 JWT,一般是 Authorization 消息頭(Authorization header)。

如今,咱們須要在後端中建立一個 express 的中間件。

中間件 isAuth

import * as jwt from 'express-jwt';

// 咱們假定 JWT 將會在 Authorization 請求頭上,可是它也能夠放在 req.body 或者 query 參數中,你只要根據業務場景選個合適的就好
const getTokenFromHeader = (req) => {
  if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
    return req.headers.authorization.split(' ')[1];
  }
}

export default jwt({
  secret: 'MySuP3R_z3kr3t', // 必須和上一節的代碼的 signature 同樣

  userProperty: 'token', // this is where the next middleware can find the encoded data generated in services/auth:generateToken -> 'req.token'

  getToken: getTokenFromHeader, // 從 request 中獲取到 auth token 的方法
})
複製代碼

建立一個能從數據庫中獲取到完整用戶記錄的中間件,而且將這些用戶信息放進 request 中。

export default (req, res, next) => {
 const decodedTokenData = req.tokenData;
 const userRecord = await UserModel.findOne({ _id: decodedTokenData._id })

 req.currentUser = userRecord;

 if(!userRecord) {
   return res.status(401).end('User not found')
 } else {
   return next();
 }
}
複製代碼

如今就能夠跳轉到用戶請求的路由了

import isAuth from '../middlewares/isAuth';
  import attachCurrentUser from '../middlewares/attachCurrentUser';
  import ItemsModel from '../models/items';

  export default (app) => {
    app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => {
      const user = req.currentUser;

      const userItems = await ItemsModel.find({ owner: user._id });

      return res.json(userItems).status(200);
    })
  }
複製代碼

通過兩個中間件訪問到的 inventory/personal-items 路由就是安全的。你須要有效的 JWT 才能訪問這個路由,固然嘍,路由也須要 JWT 中的用戶信息才能去數據庫中正確查找相應的信息。

爲何 JWT 是安全的 ?

你讀到這裏,一般會想到這麼一個問題:

Q:若是能夠在客戶端中解碼 JWT 數據的話,別人可否修改其中用戶 id 或者其它的數據呢?

A:雖然你能夠輕易地解碼 JWT,可是沒有 JWT 生成時的密鑰(Secret)就沒法對修改後的新數據進行編碼。

也是由於這個緣由,千萬不要泄漏密鑰(secret)。

咱們的服務端會在 IsAuth 這個使用了 express-jwt 庫的中間件中校驗密鑰。

如今咱們已經明白了 JWT 是如何工做的,咱們接下來去看一個很酷的功能。

如何模擬一個用戶 🕵️

用戶模擬是一種能夠在無需用戶密碼的狀況下,以一個特定用戶的身份登陸的技術。

對於超級管理員(super admins)來講,這是一個很是有用的功能,可以幫他解決或調試一個僅會話可見的用戶的問題。

沒有必要去知道用戶的密碼,只須要以正確的密鑰和必要的用戶信息來建立一個 JWT 就能夠了。

咱們來建立一個路徑,來生成模擬生成特定用戶登陸的 JWT。這個路徑只能被超級管理員帳戶使用。

首先,咱們須要爲超級管理員建立一個更高等級的角色,方法有不少,比較簡單的一種就是直接去數據庫中給用戶記錄添加一個「role」字段。

而後,咱們建立一個新的中間件來檢查用戶角色。

export default (requiredRole) => {
  return (req, res, next) => {
    if(req.currentUser.role === requiredRole) {
      return next();
    } else {
      return res.status(401).send('Action not allowed');
    }
  }
}
複製代碼

這個中間件須要放在 isAuthattachCurrentUser 以後。

最後,這個路徑將會生成一個可以模擬用戶的 JWT 。

import isAuth from '../middlewares/isAuth';
  import attachCurrentUser from '../middlewares/attachCurrentUser';
  import roleRequired from '../middlwares/roleRequired';
  import UserModel from '../models/user';

  export default (app) => {
    app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => {
      const userEmail = req.body.email;

      const userRecord = await UserModel.findOne({ email: userEmail });

      if(!userRecord) {
        return res.status(404).send('User not found');
      }

      return res.json({
        user: {
          email: userRecord.email,
          name: userRecord.name
        },
        jwt: this.generateToken(userRecord)
      })
      .status(200);
    })
  }
複製代碼

因此,這裏並無什麼黑魔法,超級管理員只須要知道須要被模擬的用戶的Email(而且這裏的邏輯與登陸十分類似,只是無需檢查口令的正確性)就能夠模擬這個用戶了。

固然,也正是由於不須要密碼,這個路徑的安全性就得靠 roleRequired 中間件來保證了。

結論 🏗️

雖然依賴第三方認證服務和庫很方便,節約了開發時間,可是咱們也須要了解認證背後的底層邏輯和原理。

在這篇文章中咱們探討了 JWT 的功能,爲何選擇一個好的加密算法很是重要,以及如何去模擬一個用戶,若是你使用的是 passport.js 這樣的庫,就很難作到這些事。

在本系列的下一篇文章中,咱們將探討經過使用 OAuth2 協議和更簡單的替代方案(如 Firebase 等第三方用於身份驗證的庫)來爲客戶提供「社交登陸」身份驗證的不一樣方法。

這裏是示例倉庫 🔬

參考資料

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索