網上有很多關於passport模塊使用方法的介紹,不過基本上都是基於express3的,本文介紹在express4框架中使用passport模塊。
前言
passport是一個功能單一,但非常強大的一個模塊,支持本地賬號驗證和第三方賬號登錄驗證,本文將介紹使用passport實現用戶注冊檢測,用戶登錄驗證。
passport是使用”策略“來驗證請求,策略是passport中最重要的概念。passport模塊本身不能做認證,所有的認證方法都以策略模式封裝為插件,需要某種認證時將其添加到package.json即可。
策略模式是一種設計模式,它將算法和對象分離開來,通過加載不同的算法來實現不同的行為,適用於相關類的成員相同但行為不同的場景,比如在passport中,認證所需的字段都是用戶名、郵箱、密碼等,但認證方法是不同的。
安裝
1)先創建工程
fan@dev:~/nodejs$ express -e node_passport fan@dev:~/nodejs$ cd node_passport/ fan@dev:~/nodejs/node_passport$npm install //進入工程文件,安裝依賴
2)安裝passport
fan@dev:~/nodejs/node_passport$ npm install passport --save
3)安裝插件
你最少需要安裝一個passport策略來使用它,一般而言本地驗證策略passport-local是必裝的。
fan@dev:~/nodejs/node_passport$ npm install passport-local --save
基本使用方法
local本地驗證
1)配置Strategies
本地驗證默認是通過用戶名和密碼來驗證的,代碼如下:
assport.use(new LocalStrategy( function(username, password, done) { //操作 }) })
也可以通過配置,采用郵箱和密碼來驗證,本文要介紹的就是通過郵箱和密碼驗證。
代碼如下:
passport.use('local.signup',new localStategy({
usernameField:'email',
passwordField:'password',
passReqToCallback:true //此處為true,下面函數的參數才能有req
},function(req,email,password,done){
//....省略代碼
})
})
注意:
- passport.use的第一個參數是一個字符串,是用來標識驗證方法的,因為一個工程中,可能會有多此驗證,每次驗證的邏輯會不一樣,下面會一一講解。
- 實例化的第一個參數是一個對象,在對象里面添加你要驗證的字段,其中passReqToCallback項,默認為false,設置為true時可以將整個req傳遞給回調函數,這樣在回調里就可以驗證req中帶的所有條件。
在工程中實現passport的配置:
新建一個config文件下,進入該文件夾目錄下,新建passport.js文件:
fan@dev:~/nodejs/node_passport$ mkdir config fan@dev:~/nodejs/node_passport$ cd config fan@dev:~/nodejs/node_passport/config$ vim passport.js
passport.js代碼如下:
var passport = require('passport'); var User = require("../models/user.js"); var localStategy = require('passport-local').Strategy; passport.serializeUser(function(user, done) { done(null, user.id); }); passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); }); passport.use('local.signup',new localStategy({ usernameField:'email', passwordField:'password', passReqToCallback:true //此處為true,下面函數的參數才能有req
},function(req,email,password,done){ req.checkBody('email','您輸入的email無效').notEmpty().isEmail(); req.checkBody('password',"您輸入了無效密碼").notEmpty().isLength({min:4}); var errors = req.validationErrors(); if(errors){ var messages = []; errors.forEach(function(error){ messages.push(error.msg); }); return done(null,false,req.flash('error',messages)); } User.findOne({'email':email},function(err,user){ if(err){ return done(err); } if(user){ return done(null,false,{message:"此郵件已經被注冊"}); } var newUser = new User(); newUser.email = email; newUser.password = newUser.encryptPassword(password); newUser.save(function(err,result){ if(err){ return done(err); } return done(null,newUser); }); }); })); passport.use('local.login',new localStategy({ usernameField:'email', passwordField:'password', passReqToCallback:true //此處為true,下面函數的參數才能有req
},function(req,email,password,done){ req.checkBody('email','您輸入的email無效').notEmpty(); req.checkBody('password',"您輸入了無效密碼").notEmpty(); var errors = req.validationErrors(); if(errors){ var messages = []; errors.forEach(function(error){ messages.push(error.msg); }); return done(null,false,req.flash('error',messages)); } User.findOne({'email':email},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); }); }));
上面代碼配置了用戶注冊時驗證和用戶登錄時驗證的策略,上面代碼使用到:
var User = require("../models/user.js"); //用戶集合模型
newUser.encryptPassword()
user.validPassword()
代碼如下:

var mongoose = require('mongoose'); var bcrypt = require('bcrypt-nodejs'); var Schema = mongoose.Schema; var userchema = new Schema({ email:{type:String,required:true}, password:{type:String,required:true} }); userchema.methods.encryptPassword = function(password){ return bcrypt.hashSync(password,bcrypt.genSaltSync(5),null); }; userchema.methods.validPassword = function(password){ return bcrypt.compareSync(password,this.password); }; module.exports = mongoose.model('User',userchema);
req.checkBody(); //通過var validator = require('express-validator'); 引入
使用方法:

在app.js文件中添加: var validator = require('express-validator'); app.use(validator());
2)驗證回調(會看config/passport.js代碼)
在passport.use()里面,done()有三種用法:
- 當發生系統級異常時,返回done(err),這里是數據庫查詢出錯,一般用next(err),但這里用done(err),兩者的效果相同,都是返回error信息;
- 當驗證不通過時,返回done(null, false, message),這里的message是可選的,可通過express-flash調用;
- 當驗證通過時,返回done(null, user)。
3)session序列化與反序列化(會看config/passport的代碼)
passport.serializeUser(function(user, done) { done(null, user.id); }); passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); });
這里第一段代碼是將環境中的user.id序列化到session中,即sessionID,同時它將作為憑證存儲在用戶cookie中。
第二段代碼是從session反序列化,參數為用戶提交的sessionID,若存在則從數據庫中查詢user並存儲與req.user中。
4)Authenticate驗證
routes/user.js中部分代碼
router.post('/signup', passport.authenticate('local.signup',{ successRedirect:'/user/profile', failureRedirect:'/user/signup', failureFlash:true }),function(req,res,next){ }); router.post('/login', passport.authenticate('local.login',{ successRedirect:'/user/profile', failureRedirect:'/user/login', failureFlash:true }),function(req,res,next){ });
這里的passport.authenticate(‘local’)就是中間件,若通過就進入后面的回調函數,並且給res加上res.user,若不通過則默認返回401錯誤。
authenticate()方法有3個參數,第一是name,即驗證策略的名稱,第二個是options,包括下列屬性:
- session:Boolean。設置是否需要session,默認為true
- successRedirect:String。設置當驗證成功時的跳轉鏈接
- failureRedirect:String。設置當驗證失敗時的跳轉鏈接
- failureFlash:Boolean or String。設置為Boolean時,express-flash將調用use()里設置的message。設置為String時將直接調用這里的信息。
- successFlash:Boolean or String。使用方法同上。
第三個參數是callback。注意如果使用了callback,那么驗證之后建立session和發出響應都應該由這個callback來做,passport中間件之后不應該再有其他中間件或callback。
5)HTTP request操作
注意上面的代碼里有個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():不帶參數。和上面的作用相反。
完整代碼:
app.js:

var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var session = require('express-session'); var exphbs = require('express-handlebars'); var passport = require('passport'); var flash = require('connect-flash'); var validator = require('express-validator'); var MongoStore = require('connect-mongo')(session); var mongoose = require('mongoose'); mongoose.connect("localhost:27017/shopping"); require('./config/passport.js'); var routes = require('./routes/index'); var userroutes = require('./routes/user'); //var users = require('./routes/users'); var app = express(); app.engine('hbs',exphbs({ defaultLayout:'main', extname:'.hbs' })); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'hbs'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(validator()); app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false, store:new MongoStore({ mongooseConnection:mongoose.connection }), cookie:{maxAge:180*60*1000} //store保存時間 })); //對session操作的模塊,應在session實例下面 app.use(flash()); app.use(passport.initialize()); app.use(passport.session()); app.use(express.static(path.join(__dirname, 'public'))); app.use(function(req,res,next){ res.locals.login = req.isAuthenticated(); next(); }); app.use('/', routes); app.use('/user', userroutes); //app.use('/users', users); // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err); }); // error handlers // development error handler // will print stacktrace if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: err }); }); } // production error handler // no stacktraces leaked to user app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: {} }); }); module.exports = app;
說明:express4以后,sessions模塊被分離出來了,為了使用req.flash,我們需要安裝express-session模塊:
var session = require('express-session'); var flash = require('connect-flash');
並配置session:
app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false, store:new MongoStore({ mongooseConnection:mongoose.connection }), cookie:{maxAge:180*60*1000} //store保存時間
}));
說明:store使用來會話存儲的,需要用到connect-mongo模塊。
config/passport.js:

var passport = require('passport'); var User = require("../models/user.js"); var localStategy = require('passport-local').Strategy; passport.serializeUser(function(user, done) { done(null, user.id); }); passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); }); passport.use('local.signup',new localStategy({ usernameField:'email', passwordField:'password', passReqToCallback:true //此處為true,下面函數的參數才能有req },function(req,email,password,done){ req.checkBody('email','您輸入的email無效').notEmpty().isEmail(); req.checkBody('password',"您輸入了無效密碼").notEmpty().isLength({min:4}); var errors = req.validationErrors(); if(errors){ var messages = []; errors.forEach(function(error){ messages.push(error.msg); }); return done(null,false,req.flash('error',messages)); } User.findOne({'email':email},function(err,user){ if(err){ return done(err); } if(user){ return done(null,false,{message:"此郵件已經被注冊"}); } var newUser = new User(); newUser.email = email; newUser.password = newUser.encryptPassword(password); newUser.save(function(err,result){ if(err){ return done(err); } return done(null,newUser); }); }); })); passport.use('local.login',new localStategy({ usernameField:'email', passwordField:'password', passReqToCallback:true //此處為true,下面函數的參數才能有req },function(req,email,password,done){ req.checkBody('email','您輸入的email無效').notEmpty(); req.checkBody('password',"您輸入了無效密碼").notEmpty(); var errors = req.validationErrors(); if(errors){ var messages = []; errors.forEach(function(error){ messages.push(error.msg); }); return done(null,false,req.flash('error',messages)); } User.findOne({'email':email},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); }); }));
views/signup.ejs:

<%- include header %> <form method="post"> <% if(hasError){ %> <div class="alert alert-danger"> <% messages.forEach(function(item){ %> <p> <%= item %> </p> <% }) %> </div> <% } %> 郵箱: <input type="text" name="email" /><br/> 密碼: <input type="password" name="password" /><br /> 確認密碼:<input type="password" name="password-repeat" /><br /> <input type="submit" value="注冊" /> </form> <%- include footer %>
測試:
在瀏覽器中輸入localhost:3000/users/signup
密碼位數少於4的情況(右圖是提交之后的顯示):
郵箱錯誤:
郵箱和密碼都錯誤:
這里只舉例用戶的注冊,用戶登錄其實也差不多,這里就不舉例了。