基于 Passport.js 的權限認證

      網友投稿 984 2025-04-02

      # 基于 Passport.js 的權限認證

      參考了 [Passport.js 學習筆記](http://ju.outofmemory.cn/entry/99459)與 [Wiki.js](https://github.com/Requarks/wiki) 的源代碼

      認證又稱 “ 驗證 ”、“ 鑒權 ”,是指通過一定的手段,完成對用戶身份的確認。身份驗證的方法有很多,基本上可分為:基于共享密鑰的身份驗證、基于生物學特征的身份驗證和基于公開密鑰加密算法的身份驗證。

      登陸認證,是用戶在訪問應用或者網站時,通過是先注冊的用戶名和密碼,告訴應用使用者的身份,從而獲得訪問權限的一種操作。

      幾乎所有的應用都需要登陸認證! Passport.js 是 Node.js 中的一個做登錄驗證的中間件,極其靈活和模塊化,并且可與 Express、Sails 等 Web 框架無縫集成。Passport 功能單一,即只能做登錄驗證,但非常強大,支持本地賬號驗證和第三方賬號登錄驗證(OAuth 和 OpenID 等),支持大多數 Web 網站和服務。

      策略(Strategy )是 passport 中最重要的概念。passport 模塊本身不能做認證,所有的認證方法都以策略模式封裝為插件,需要某種認證時將其添加到 package.json 即可。策略模式是一種設計模式,它將算法和對象分離開來,通過加載不同的算法來實現不同的行為,適用于相關類的成員相同但行為不同的場景,比如在 passport 中,認證所需的字段都是用戶名、郵箱、密碼等,但認證方法是不同的。依據策略模式,passport 支持了眾多的驗證方案,包括 Basic、Digest 、 OAuth(1.0 ,和 2.0 的三種實現)、 JWT 等。

      # 策略配置

      ## 本地認證

      ```js

      const LocalStrategy = require("passport-local").Strategy;

      passport.use(

      "local",

      new LocalStrategy(

      {

      usernameField: "email",

      passwordField: "password"

      },

      (uEmail, uPassword, done) => {

      db.User.findOne({ email: uEmail, provider: "local" })

      .then(user => {

      if (user) {

      // validatePassword 是 User 模型自帶的數據校驗輔助函數

      return user

      .validatePassword(uPassword)

      .then(() => {

      return done(null, user) || true;

      })

      .catch(err => {

      return done(err, null);

      });

      } else {

      return done(new Error("INVALID_LOGIN"), null);

      }

      })

      .catch(err => {

      done(err, null);

      });

      }

      )

      );

      ```

      如果使用 MySQL、PostgreSQL 等關系型數據庫,我們也可以進行

      ```js

      // 綁定對于用戶密碼進行加密的操作

      userSchema.statics.hashPassword = rawPwd => {

      return bcrypt.hash(rawPwd);

      };

      // 綁定對于密碼的驗證操作

      userSchema.methods.validatePassword = function(rawPwd) {

      return bcrypt.compare(rawPwd, this.password).then(isValid => {

      return isValid

      ? true

      : Promise.reject(new Error(lang.t("auth:errors:invalidlogin")));

      });

      };

      ```

      注意,這里的字段名稱應該是頁面表單提交的名稱,即 `req.body.xxx`,而不是 user 數據庫中的字段名稱。

      將 options 作為 LocalStrategy 第一個參數傳入即可。

      passport 本身不處理驗證,驗證方法在策略配置的回調函數里由用戶自行設置,它又稱為驗證回調。驗證回調需要返回驗證結果,這是由 done() 來完成的。

      在 passport.use() 里面,done() 有三種用法:

      當發生系統級異常時,返回 done(err),這里是數據庫查詢出錯,一般用 next(err),但這里用 done(err),兩者的效果相同,都是返回 error 信息;當驗證不通過時,返回 done(null, false, message),這里的 message 是可選的,可通過 express-flash 調用;當驗證通過時,返回 done(null, user)。

      ## 混合策略

      ```js

      const passport = require('passport')

      , LocalStrategy = require('passport-local').Strategy

      , AnonymousStrategy = require('passport-anonymous').Strategy;

      ...

      // 匿名登錄認證作為本地認證的 fallback

      passport.use(new AnonymousStrategy());

      ...

      app.get('/',

      passport.authenticate(['local', 'anonymous'], { session: false }),

      function(req, res){

      if (req.user) {

      res.json({ msg: "用戶已登錄"});

      } else {

      res.json({ msg: "用戶以匿名方式登錄"});

      }

      });

      ```

      # 框架集成

      ## 登錄認證

      ```js

      const express = require('express');

      const cookieParser = require('cookie-parser');

      const session = require('express-session');

      const flash = require('express-flash');

      const passport = require('passport');

      ...

      // 在使用 app.use 之前需要進行 passport 的配置

      app.use(cookieParser());

      app.use(session({...}));

      app.use(flash())

      app.use(passport.initialize());

      app.use(passport.session());

      ...

      const ExpressBrute = require('express-brute')

      const ExpressBruteMongooseStore = require('express-brute-mongoose')

      ```

      ```js

      app.post(

      "/login",

      passport.authenticate("local", {

      successRedirect: "/",

      failureRedirect: "/login",

      failureFlash: true

      }),

      function(req, res) {

      // 驗證成功則調用此回調函數

      res.redirect("/users/" + req.user.username);

      }

      );

      ```

      ```js

      // controllers/auth.js

      ...

      // 使用 ExpressBruteMongooseStore 來存放爆破信息,也可以使用 MemoryStore 將信息存放于內存

      const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)

      const bruteforce = new ExpressBrute(EBstore, {

      freeRetries: 5,

      minWait: 60 * 1000,

      maxWait: 5 * 60 * 1000,

      refreshTimeoutOnRequest: false,

      failCallback (req, res, next, nextValidRequestDate) {

      req.flash('alert', {

      class: 'error',

      title: lang.t('auth:errors.toomanyattempts'),

      message: lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),

      iconClass: 'fa-times'

      })

      res.redirect('/login')

      }

      })

      // 處理來自表單提交中包含的登錄信息

      router.post('/login', bruteforce.prevent, function (req, res, next) {

      new Promise((resolve, reject) => {

      // [1] LOCAL AUTHENTICATION

      passport.authenticate('local', function (err, user, info) {

      if (err) { return reject(err) }

      if (!user) { return reject(new Error('INVALID_LOGIN')) }

      resolve(user)

      })(req, res, next)

      }).then((user) => {

      // LOGIN SUCCESS

      // 執行用戶登錄操作,將用戶 ID 寫入到 Session 中

      return req.logIn(user, function (err) {

      if (err) { return next(err) }

      req.brute.reset(function () {

      return res.redirect('/')

      })

      }) || true

      }).catch(err => {

      // LOGIN FAIL

      if (err.message === 'INVALID_LOGIN') {

      req.flash('alert', {

      title: lang.t('auth:errors.invalidlogin'),

      message: lang.t('auth:errors.invalidloginmsg')

      })

      return res.redirect('/login')

      } else {

      req.flash('alert', {

      title: lang.t('auth:errors.loginerror'),

      message: err.message

      })

      return res.redirect('/login')

      }

      })

      })

      ...

      ```

      ```js

      const router = express.Router();

      ```

      [koa-passport](https://github.com/rkusa/koa-passport)

      ```js

      // body parser

      const bodyParser = require("koa-bodyparser");

      app.use(bodyParser());

      // Sessions

      const session = require("koa-session");

      app.keys = ["secret"];

      app.use(session({}, app));

      const passport = require("koa-passport");

      app.use(passport.initialize());

      app.use(passport.session());

      ```

      ## 訪問校驗

      注意上面的代碼里有個 req.logIn(),它不是 http 模塊原生的方法,也不是 express 中的方法,而是 passport 加上的,passport 擴展了 HTTP request,添加了四種方法。

      logIn(user, options, callback) :用 login() 也可以。作用是為登錄用戶初始化 session。options 可設置 session 為 false,即不初始化 session,默認為 true。 logOut() :別名為 logout()。作用是登出用戶,刪除該用戶 session。不帶參數。 isAuthenticated() :不帶參數。作用是測試該用戶是否存在于 session 中(即是否已登錄)。若存在返回 true。事實上這個比登錄驗證要用的更多,畢竟 session 通常會保留一段時間,在此期間判斷用戶是否已登錄用這個方法就行了。 isUnauthenticated() :不帶參數。和上面的作用相反。

      驗證用戶提交的憑證是否正確,是與 session 中儲存的對象進行對比,所以涉及到從 session 中存取數據,需要做 session 對象序列化與反序列化。調用代碼如下:

      ```js

      // 獲取用戶編號,用于在 logIn 方法執行時向 Session 中寫入用戶編號,ID 或者 Token 皆可

      passport.serializeUser(function(user, done) {

      done(null, user._id);

      });

      // 根據 ID 查找用戶,也是為了判斷用戶是否存在

      passport.deserializeUser(function(id, done) {

      db.User.findById(id)

      .then(user => {

      if (user) {

      done(null, user);

      } else {

      done(new Error(lang.t("auth:errors:usernotfound")), null);

      }

      return true;

      })

      .catch(err => {

      done(err, null);

      });

      });

      ```

      這里第一段代碼是將環境中的 user.id 序列化到 session 中,即 sessionID,同時它將作為憑證存儲在用戶 cookie 中。

      第二段代碼是從 session 反序列化,參數為用戶提交的 sessionID,若存在則從數據庫中查詢 user 并存儲與 req.user 中。

      ```js

      //這里getUser方法需要自定義

      app.get("/user", isAuthenticated, getUser);

      // 將req.isAuthenticated()封裝成中間件

      module.exports = (req, res, next) => {

      // 判斷用戶是否經過認證

      if (!req.isAuthenticated()) {

      if (req.app.locals.appconfig.public !== true) {

      return res.redirect("/login");

      } else {

      req.user = rights.guest;

      res.locals.isGuest = true;

      }

      } else {

      res.locals.isGuest = false;

      }

      // 進行角色的權限校驗

      res.locals.rights = rights.check(req);

      if (!res.locals.rights.read) {

      return res.render("error-forbidden");

      }

      // Expose user data

      res.locals.user = req.user;

      return next();

      };

      ```

      ```js

      app.get("/logout", function(req, res) {

      req.logout();

      res.redirect("/");

      });

      ```

      # OAuth

      ```

      * OAuth 驗證策略概述

      *

      * 當用戶點擊 “ 使用 XX 登錄 ” 鏈接

      * * 若用戶已登錄

      * * 檢查該用戶是否已綁定 XX 服務

      * ? ? - 如果已綁定,返回錯誤(不允許賬戶合并)

      * ? ? - 否則開始驗證流程,為該用戶綁定XX服務

      * * 用戶未登錄

      * * 檢查是否老用戶

      * ? ? - 如果是老用戶,則登錄

      * ? ? - 否則檢查OAuth返回profile中的email,是否在用戶數據庫中存在

      * ? ? ? - 如果存在,返回錯誤信息

      * ? ? ? - 否則創建一個新賬號

      ```

      ```js

      const OAuth2Strategy = require('passport-oauth').OAuth2Strategy;

      passport.use('provider', new OAuth2Strategy({

      authorizationURL: 'https://www.provider.com/oauth2/authorize',

      tokenURL: 'https://www.provider.com/oauth2/token',

      clientID: '123-456-789',

      clientSecret: 'shhh-its-a-secret'

      callbackURL: 'https://www.example.com/auth/provider/callback'

      },

      function(accessToken, refreshToken, profile, done) {

      User.findOrCreate(..., function(err, user) {

      done(err, user);

      });

      }

      ));

      ```

      refreshToken 是重新獲取 access token 的方法,因為 access token 是有使用期限的,到期了必須讓用戶重新授權才行,現在有了 refresh token,你可以讓應用定期的用它去更新 access token,這樣第三方服務就可以一直綁定了。不過這個方法并不是每個服務商都提供,注意看服務商的文檔。

      ```js

      const GitHubStrategy = require("passport-github2").Strategy;

      passport.use(

      "github",

      new GitHubStrategy(

      {

      clientID: appconfig.auth.github.clientId,

      clientSecret: appconfig.auth.github.clientSecret,

      callbackURL: appconfig.host + "/login/github/callback",

      scope: ["user:email"]

      },

      (accessToken, refreshToken, profile, cb) => {

      db.User.processProfile(profile)

      .then(user => {

      return cb(null, user) || true;

      })

      .catch(err => {

      return cb(err, null) || true;

      });

      }

      )

      );

      ```

      ```js

      router.get(

      "/login/ms",

      passport.authenticate("windowslive", {

      scope: ["wl.signin", "wl.basic", "wl.emails"]

      })

      );

      router.get(

      "/login/google",

      passport.authenticate("google", { scope: ["profile", "email"] })

      );

      router.get(

      "/login/facebook",

      passport.authenticate("facebook", { scope: ["public_profile", "email"] })

      );

      router.get(

      "/login/github",

      passport.authenticate("github", { scope: ["user:email"] })

      );

      router.get(

      "/login/slack",

      passport.authenticate("slack", {

      scope: ["identity.basic", "identity.email"]

      })

      );

      router.get("/login/azure", passport.authenticate("azure_ad_oauth2"));

      router.get(

      "/login/ms/callback",

      passport.authenticate("windowslive", {

      failureRedirect: "/login",

      successRedirect: "/"

      })

      );

      router.get(

      "/login/google/callback",

      passport.authenticate("google", {

      failureRedirect: "/login",

      successRedirect: "/"

      })

      );

      router.get(

      "/login/facebook/callback",

      passport.authenticate("facebook", {

      failureRedirect: "/login",

      successRedirect: "/"

      基于 Passport.js 的權限認證

      })

      );

      router.get(

      "/login/github/callback",

      passport.authenticate("github", {

      failureRedirect: "/login",

      successRedirect: "/"

      })

      );

      router.get(

      "/login/slack/callback",

      passport.authenticate("slack", {

      failureRedirect: "/login",

      successRedirect: "/"

      })

      );

      router.get(

      "/login/azure/callback",

      passport.authenticate("azure_ad_oauth2", {

      failureRedirect: "/login",

      successRedirect: "/"

      })

      );

      ```

      [Passport-GitHub strategy.js](https://github.com/jaredhanson/passport-github/blob/master/lib/strategy.js)

      passport 以插件的形式支持了很多第三方網站和服務的 OAuth 驗證,但并不是所有的,如果你需要在 app 中用到第三方的服務,但它們沒有對應的 passport 插件,你可以用通用的 OAuth 或其他驗證方法來進行驗證,也可以將它們封裝成 passport-x 插件。

      JavaScript

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:excel如何顯示網格線(Excel顯示網格線)
      下一篇:excel2007背景如何打印
      相關文章
      亚洲人成色77777在线观看大| 亚洲黄片毛片在线观看| 亚洲午夜爱爱香蕉片| 午夜在线a亚洲v天堂网2019| 亚洲精品福利视频| 亚洲国产一区国产亚洲| 亚洲高清国产拍精品26U| 亚洲A∨无码一区二区三区| 亚洲国产成人片在线观看无码| 亚洲人成人无码网www电影首页| 久久久久亚洲AV成人网人人网站 | 国产成人精品久久亚洲高清不卡 | 亚洲精品成人区在线观看| 亚洲天堂中文字幕在线| 亚洲AV伊人久久青青草原| 亚洲国模精品一区| 亚洲日韩中文字幕在线播放| 亚洲成a人片在线观看无码专区| 亚洲精品中文字幕无码蜜桃| 亚洲国产另类久久久精品 | 亚洲av无码专区亚洲av不卡| 亚洲youwu永久无码精品| 日本系列1页亚洲系列| 亚洲国产免费综合| 亚洲精品国产字幕久久不卡| 亚洲AV午夜成人影院老师机影院| 亚洲视频精品在线| 亚洲国产成人手机在线电影bd | 国产gv天堂亚洲国产gv刚刚碰| 亚洲色欲一区二区三区在线观看| 久久亚洲高清观看| 亚洲电影在线免费观看| 成人亚洲国产va天堂| 大桥未久亚洲无av码在线| 亚洲精品无码av天堂| 亚洲中文字幕无码久久精品1 | 中文字幕在线观看亚洲日韩| 欧美亚洲国产SUV| 一本久到久久亚洲综合| 国产L精品国产亚洲区久久| 亚洲国产一成人久久精品|