基本用法

2024-12-03 17:45 更新

passport用的比較多的有l(wèi)ocal本地驗(yàn)證和OAuth驗(yàn)證,這里講一下兩者的使用。

你也可以看看張丹寫的這兩篇,Express結(jié)合Passport實(shí)現(xiàn)登陸認(rèn)證Passport實(shí)現(xiàn)社交網(wǎng)絡(luò)OAuth登陸,里面的示例覆蓋了基本的用法,本文也參考了其中的一些例子。

local本地驗(yàn)證

本地驗(yàn)證默認(rèn)使用用戶名和密碼來進(jìn)行驗(yàn)證。

配置策略

在做驗(yàn)證之前,首先需要對(duì)策略進(jìn)行配置,官方的示例如下:

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

passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function(err, user) {
      if (err) { return done(err); }
      if (!user) {
        return done(null, false, { message: '用戶名不存在.' });
      }
      if (!user.validPassword(password)) {
        return done(null, false, { message: '密碼不匹配.' });
      }
      return done(null, user);
    });
  }
));

其中的User.findOne()是MongoDB風(fēng)格的語(yǔ)法,意思是從數(shù)據(jù)庫(kù)的User集合中查詢一條數(shù)據(jù),第一個(gè)參數(shù)是查詢條件,后面是callback,一般在callback中進(jìn)行后續(xù)操作。

這里的邏輯很簡(jiǎn)單,依次檢查username、password,如果出錯(cuò)則返回錯(cuò)誤信息,如果通過則返回done(null,user)。

usernameField

前面說過passport默認(rèn)使用用戶名和密碼來驗(yàn)證,但實(shí)際上也有很多需要用郵箱來驗(yàn)證的,那么如何實(shí)現(xiàn)呢?

passport在策略配置里提供了options參數(shù),用來設(shè)置你要驗(yàn)證的字段名稱,即usernameField,使用方法如下:

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'passwd'
  },
  function(username, password, done) {
    // ...
  }
));

注意,這里的字段名稱應(yīng)該是頁(yè)面表單提交的名稱,即req.body.xxx,而不是user數(shù)據(jù)庫(kù)中的字段名稱。

將options作為L(zhǎng)ocalStrategy第一個(gè)參數(shù)傳入即可。

驗(yàn)證回調(diào)

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

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

  • 當(dāng)發(fā)生系統(tǒng)級(jí)異常時(shí),返回done(err),這里是數(shù)據(jù)庫(kù)查詢出錯(cuò),一般用next(err),但這里用done(err),兩者的效果相同,都是返回error信息;
  • 當(dāng)驗(yàn)證不通過時(shí),返回done(null, false, message),這里的message是可選的,可通過express-flash調(diào)用;
  • 當(dāng)驗(yàn)證通過時(shí),返回done(null, user)。

密碼驗(yàn)證

在張丹的教程里密碼是明文存儲(chǔ)的,在實(shí)際中這當(dāng)然不行,上面的代碼里是user.validPassword(password)方法,這并不是passport添加的,而是需要用戶自定義。

一般對(duì)密碼進(jìn)行哈希和鹽化的Nodejs模塊是bcrypt,它提供一個(gè)compare方法來驗(yàn)證密碼,如何使用它則超出本文的范圍,這里就不講了。

 session序列化與反序列化

驗(yàn)證用戶提交的憑證是否正確,是與session中儲(chǔ)存的對(duì)象進(jìn)行對(duì)比,所以涉及到從session中存取數(shù)據(jù),需要做session對(duì)象序列化與反序列化。調(diào)用代碼如下:

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

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

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

這段代碼的順序可以放在passport.use()的前面或后面,但需要在app.configure()之前。

Authenticate驗(yàn)證

做完了上面這些設(shè)置,我們終于可以開始做驗(yàn)證了。

app.post('/login',
  passport.authenticate('local',
    { successRedirect: '/',
     failureRedirect: '/login',
     failureFlash: true }),
  function(req, res) {
    // 驗(yàn)證成功則調(diào)用此回調(diào)函數(shù)
    res.redirect('/users/' + req.user.username);
  });

這里的passport.authenticate(‘local’)就是中間件,若通過就進(jìn)入后面的回調(diào)函數(shù),并且給res加上res.user,若不通過則默認(rèn)返回401錯(cuò)誤。

authenticate()方法有3個(gè)參數(shù),第一是name,即驗(yàn)證策略的名稱,第二個(gè)是options,包括下列屬性:

  • session:Boolean。設(shè)置是否需要session,默認(rèn)為true
  • successRedirect:String。設(shè)置當(dāng)驗(yàn)證成功時(shí)的跳轉(zhuǎn)鏈接
  • failureRedirect:String。設(shè)置當(dāng)驗(yàn)證失敗時(shí)的跳轉(zhuǎn)鏈接
  • failureFlash:Boolean or String。設(shè)置為Boolean時(shí),express-flash將調(diào)用use()里設(shè)置的message。設(shè)置為String時(shí)將直接調(diào)用這里的信息。
  • successFlash:Boolean or String。使用方法同上。

第三個(gè)參數(shù)是callback。注意如果使用了callback,那么驗(yàn)證之后建立session和發(fā)出響應(yīng)都應(yīng)該由這個(gè)callback來做,passport中間件之后不應(yīng)該再有其他中間件或callback。以下是代碼:

app.get('/login', function(req, res, next) {
  passport.authenticate('local', function(err, user, info) {
    if (err) { return next(err); }
    if (!user) { return res.redirect('/login'); }
    req.logIn(user, function(err) {
      if (err) { return next(err); }
      return res.redirect('/users/' + user.username);
    });
  })(req, res, next);
});

HTTP request操作

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

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

完整示例

基本上passport本地驗(yàn)證的知識(shí)點(diǎn)就是這些,下面給出一個(gè)相對(duì)完整的示例,包括bcrypt的實(shí)現(xiàn),這里借用了nodeclub中的方法,為實(shí)現(xiàn)它你需要自己配置hash:

var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
//User模型需自己實(shí)現(xiàn)
var User = require('../models/User');
var bcrypt = require('bcrypt');

passport.serializeUser(function(user, done) {
  done(null, user.id);
});

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

//這里的username可以改成前端表單對(duì)應(yīng)的命名,如:
// <form><input type="text" name="hehe">...</form>
//則這里將所有的username改為hehe
passport.use(new LocalStrategy({ usernameField: 'username' }, function(username, password, done) {
  //實(shí)現(xiàn)用戶名或郵箱登錄
  //這里判斷提交上的username是否含有@,來決定查詢的字段是哪一個(gè)
  var criteria = (username.indexOf('@') === -1) ? {username: username} : {email: username};
  User.findOne(criteria, function(err, user) {
    if (!user) return done(null, false, { message: '用戶名或郵箱 ' + username + ' 不存在'});
    bcompare(password, hash, function(err, isMatch) {
      if (isMatch) {
        return done(null, user);
      } else {
        return done(null, false, { message: '密碼不匹配' });
      }
    });
  });
}));

...

app.use(cookieParser());
app.use(session({secret: "need change"}));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());

...

app.post('/login', passport.authenticate('local', function(err, user, info) {
    if (err) return next(err);
    if (!user) {
      req.flash('errors', { msg: info.message });
      return res.redirect('/login');
    }
    req.logIn(user, function(err) {
      if (err) return next(err);
      req.flash('success', { msg: '登錄成功!' });
      res.redirect('/');
    });
  })(req, res, next)
);
//這里getUser方法需要自定義
app.get('/user', isAuthenticated, getUser);
app.get('/logout', function(req, res){
  req.logout();
  res.redirect('/');
});

//將req.isAuthenticated()封裝成中間件
var isAuthenticated = function(req, res, next) {
  if (req.isAuthenticated()) return next();
  res.redirect('/login');
};

var bcompare = function (str, hash, callback) {
bcrypt.compare(str, hash, callback);
};

passport-local模塊也包括一些示例,不過這些示例都是Express 3.x時(shí)代寫的,所以不要原封不動(dòng)的copy代碼。

基本的local驗(yàn)證就講到這里,下面還有進(jìn)階的驗(yàn)證技巧,比如在RESTful API中使用passport,驗(yàn)證多個(gè)條件等。

OAuth驗(yàn)證

OAuth驗(yàn)證是體現(xiàn)passport強(qiáng)大的地方,如果你看過nodeclub的源碼,會(huì)發(fā)現(xiàn)它自己實(shí)現(xiàn)了local驗(yàn)證,但它的Github驗(yàn)證是用passport來實(shí)現(xiàn)的。

OAuth標(biāo)準(zhǔn)分為兩個(gè)版本,1.0版和2.0版,兩者被使用的都很廣泛,passport通過passport-oauth為兩者提供支持,使用下面的命令可以安裝。

npm install passport-oauth

OAuth驗(yàn)證流程

OAuth1.0和2.0的使用流程都差不多,一般來說如下:

  1. 為你的app去第三方服務(wù)商處申請(qǐng)標(biāo)識(shí)和令牌appkey和secret;
  2. 在你的app里添加按鈕或鏈接,將用戶引導(dǎo)至服務(wù)商的授權(quán)頁(yè),用戶在這里選擇授權(quán)給你的app;
  3. 授權(quán)成功后跳轉(zhuǎn)回你的app,同時(shí)還傳遞回access_token和一些用戶資料。

到這里首次驗(yàn)證流程就完成了,之后只要拿access_token去就可以做登錄驗(yàn)證或者其他事了。

OAuth1.0

要使用passport OAuth1.0驗(yàn)證你需要先引入:

var passport = require('passport')
  , OAuthStrategy = require('passport-oauth').OAuthStrategy;

然后是配置:

passport.use('provider', new OAuthStrategy({
    requestTokenURL: 'https://www.provider.com/oauth/request_token',
    accessTokenURL: 'https://www.provider.com/oauth/access_token',
    userAuthorizationURL: 'https://www.provider.com/oauth/authorize',
    consumerKey: '123-456-789',
    consumerSecret: 'shhh-its-a-secret'
    callbackURL: 'https://www.example.com/auth/provider/callback'
  },
  function(token, tokenSecret, profile, done) {
    User.findOrCreate(..., function(err, user) {
      done(err, user);
    });
  }
));

這里比通用流程多的一點(diǎn)就是,你的App需要先訪問第三方服務(wù),獲取request token,這個(gè)request token是未授權(quán)的,等用戶授權(quán)之后,可以拿這個(gè)request token去換取access token。

在passport中你不必管這些細(xì)節(jié),找到第三服務(wù)的文檔找到對(duì)應(yīng)的URL添上即可。當(dāng)然你還得申請(qǐng)key和secret。

use方法的回調(diào)接受四個(gè)參數(shù),token就是access token,和tokenSecret一起好好保存。profile則是用戶在第三方服務(wù)上的一些公開資料,它的模型在這里,不過返回的資料不一定全面,在使用前需要驗(yàn)證是否存在。

OAuth1.0的路由常見寫法如下:

app.get('/auth/provider', passport.authenticate('provider'));
app.get('/auth/provider/callback', 
  passport.authenticate('provider', { successRedirect: '/',
                                      failureRedirect: '/login' }));

上面就是OAuth1.0的驗(yàn)證流程。

OAuth1.0主要是一些比較早提供第三方登錄功能的網(wǎng)站使用,現(xiàn)在的網(wǎng)站大部分使用OAuth2.0了,新浪微博原先使用的是1.0,現(xiàn)在也改用2.0了。

OAuth2.0

OAuth2.0的驗(yàn)證不需要request_token,但比1.0多了scope和refresh token,我們先來看看具體的配置方法:

var passport = require('passport')
  , 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的方法,因?yàn)閍ccess token是有使用期限的,到期了必須讓用戶重新授權(quán)才行,現(xiàn)在有了refresh token,你可以讓應(yīng)用定期的用它去更新access token,這樣第三方服務(wù)就可以一直綁定了。不過這個(gè)方法并不是每個(gè)服務(wù)商都提供,注意看服務(wù)商的文檔。

下面是路由,OAuth2.0也有一點(diǎn)不同:

app.get('/auth/provider',
  passport.authenticate('provider', { scope: 'email' })
);
app.get('/auth/provider/callback', 
  passport.authenticate('provider', { successRedirect: '/',
                                      failureRedirect: '/login' }));

scope是權(quán)限范圍,需要在服務(wù)商處事先申請(qǐng),想進(jìn)一步了解可參考微博的scope文檔。它可以只有一項(xiàng),也可以有多項(xiàng),當(dāng)為多項(xiàng)時(shí)以數(shù)組形式表示。

使用passport-x插件

passport-oauth包含通用的驗(yàn)證方法,基本山任何提供OAuth的服務(wù)都能用上面的方法來驗(yàn)證,但大部分提供第三方登錄的網(wǎng)站都有passport的插件,它們的列表見官網(wǎng)Github wiki。使用它們可以讓app綁定第三方服務(wù)更加簡(jiǎn)單和模塊化。

passport-x插件的一般用法如下(以Github為例)。

首先安裝passport-github,注意這種情況不需要安裝passport-oauth:

npm install passport-github

安裝完后是配置:

var passport = require('passport')
  , GithubStrategy = require('passport-github').Strategy;

//passport設(shè)置部分 
passport.use(new GithubStrategy({
    clientID: GITHUB_CLIENT_ID,
    clientSecret: GITHUB_CLIENT_SECRET,
    callbackURL: "http://www.example.com/auth/github/callback"
  },
  function(accessToken, refreshToken, profile, done) {
    User.findOrCreate(..., function(err, user) {
      if (err) { return done(err); }
      done(null, user);
    });
  }
));

...
//路由部分
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  function(req, res) {
    res.redirect('/');
  });

與通用OAuth驗(yàn)證流程對(duì)比,上面的代碼少了服務(wù)商的驗(yàn)證頁(yè)部分,你只需要將獲得的appkey和secret填到對(duì)應(yīng)地方即可。

OAuth驗(yàn)證的邏輯

OAuth驗(yàn)證的麻煩之處主要是處理邏輯,很多網(wǎng)站將第三方的OAuth作為一種用戶注冊(cè)手段,當(dāng)用戶點(diǎn)擊第三方登錄時(shí),若用戶未注冊(cè)會(huì)為他們創(chuàng)建賬號(hào),這里面的邏輯就比較繞了。比如Hackathon Starter的處理邏輯如下:

/**
 * OAuth驗(yàn)證策略概述
 * 
 * 當(dāng)用戶點(diǎn)擊“使用XX登錄”鏈接
 * - 若用戶已登錄
 *   - 檢查該用戶是否已綁定XX服務(wù)
 *     - 如果已綁定,返回錯(cuò)誤(不允許賬戶合并)
 *     - 否則開始驗(yàn)證流程,為該用戶綁定XX服務(wù)
 * - 用戶未登錄
 *   - 檢查是否老用戶
 *     - 如果是老用戶,則登錄
 *     - 否則檢查OAuth返回profile中的email,是否在用戶數(shù)據(jù)庫(kù)中存在
 *       - 如果存在,返回錯(cuò)誤信息
 *       - 否則創(chuàng)建一個(gè)新賬號(hào)
 */

另外還有平常驗(yàn)證用戶是否已綁定某個(gè)服務(wù),可以封裝成中間件:

var isAuthorized = function(req, res, next) {

  if (req.user.provider)) {
    next();
  } else {
    //do something else
  }
};

學(xué)習(xí)OAuth驗(yàn)證最好的項(xiàng)目是Hackathon Starter,它實(shí)現(xiàn)了十幾種的第三方網(wǎng)站和服務(wù)的OAuth驗(yàn)證,推薦學(xué)習(xí)。下面進(jìn)階學(xué)習(xí)里面還有如何開發(fā)一個(gè)passport OAuth驗(yàn)證插件。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)