
1.基礎(chǔ)
- Passport使用不同的策略(>300種)進行授權(quán),最常見的是本地策略,本地策略中最常見的又是用戶名密碼策略
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
try{
//If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure.An additional info message can be supplied to indicate the reason for the failure. This is useful for displaying a flash message prompting the user to try again.
if (username!='aaa') {
return done(null, false, { message: 'Incorrect username.' });
}
if (password!='aaa') {
return done(null, false, { message: 'Incorrect password.' });
}
//If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated.
return done(null, user);
}
catch(err){
return done(err);
}
}
));
著重說一下上面的callback,也就是done()方法
- 如果驗證通過,則將登陸的用戶返回:
done(null,user) - 如果驗證未通過,比如,用戶名密碼錯誤,則返回
done(null,false),還可以提供額外信息done(null, false, { message: 'Incorrect password.' }); - 如果發(fā)生異常,則
done(err)
2.將passport作為express中間件
- express應(yīng)用需要
passport.initialize()來進行啟動
如果打算使用基于session的驗證(非SPA、瀏覽器最常用這種方法),需要使用passport.session()和session()中間件,(session()要在passport.session()之前聲明引用),在express4.x中的寫法如下:
var express = require('express');
var session = require("express-session");
var bodyParser = require("body-parser");
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var app = express();
app.use(express.static("public"));
app.use(session({ secret: "cats" }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
3. Session
一個典型的網(wǎng)絡(luò)應(yīng)用,授權(quán)過程只有在login過程中發(fā)生,如果驗證成功,服務(wù)器將建立一個session,并在瀏覽器端建立一個cookie.
再往后的請求都不會再次請求憑證。但瀏覽器有一個唯一的cookie,對應(yīng)服務(wù)端相應(yīng)的session。為了支持登錄session驗證,passport在session中對user進行序列化和反序列化:
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
注意到,只對userid進行了序列化操作,這是為了減小session體積。在接下來的請求中,這個id用來查找user,并存儲在req.user。
序列化和反序列化由應(yīng)用程序定義,可自由選擇數(shù)據(jù)庫或者objectmapper方法。這里與驗證層無關(guān)。
4. Username & Password驗證方式舉例:
最廣泛使用的就是用戶名、密碼驗證方式。 passport-local 模塊支持這種方式
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: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
代碼基本和前面一樣,只不過這里使用數(shù)據(jù)庫查詢來確定用戶身份。
- 前端頁面:
<form action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
- 后端路由
使用authenticate()和local策略的路由
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true })
);
后面的三個參數(shù)分別是成功跳轉(zhuǎn)、失敗跳轉(zhuǎn)和消息閃現(xiàn)
消息閃現(xiàn)只在瀏覽器出現(xiàn)一次,然后就被銷毀閱后即焚。當(dāng)其設(shè)置為true時,錯誤message將會被發(fā)送到客戶端:
if (err) { return done(err); }
前端使用{{error.message}}就能拿到消息
- 默認(rèn)情況下passport使用
username和password,也可以自由定義:
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd'
},
function(username, password, done) {
// ...
}
));
5. login和logout方法
passport在req上暴露這兩個方法,可以直接使用:
req.login(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + req.user.username);
});
login方法執(zhí)行完畢后,user對象將會被賦值給req.user。注意passport.authenticate()中間件自動調(diào)用login方法。無需手動調(diào)用。需要調(diào)用此方法的時候是用戶注冊成功后自動登陸的場景。
app.get('/logout', function(req, res){
req.logout();
res.redirect('/');
});
logout方法將清除req.user和服務(wù)端session
6.其它相關(guān)包
如果打算使用mongoose,passport-local-mongoose包(以下簡稱plm)是一個mongoose插件,將會簡化username和password存儲流程:github
使用mongoose+passport安裝的典型依賴:
npm install passport passport-local mongoose passport-local-mongoose --save
使用passport-local-mongoose
6.1 在領(lǐng)域類中定義 plugin:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const passportLocalMongoose = require('passport-local-mongoose');
const User = new Schema({});
//plugin可接受option參數(shù)
User.plugin(passportLocalMongoose);
module.exports = mongoose.model('User', User);
你可以自由定義user類,plm將自動為其加入username、hash和salt字段,以及一些額外的方法
User.plugin(passportLocalMongoose, options);
Main Options:
saltlen: 鹽長度. Default: 32
iterations: 哈希算法 iterations長度. Default: 25000
keylen: key長度. Default: 512
digestAlgorithm: 加密算法,Default: sha256.
interval: login允許間隔時間. Default: 100
usernameField: 自定義username字段名稱. Defaults to 'username'. 比如可以改為"email".
usernameUnique:username字段是否唯一. Defaults to true.
saltField: salt字段名稱. Defaults to 'salt'.
hashField: hash字段名稱. Defaults to 'hash'.
attemptsField: 用戶登陸嘗試次數(shù)字段名稱. Defaults to 'attempts'.
lastLoginField: 最后登陸時間戳字段名稱. Defaults to 'last'.
selectFields: 定義在mongodb中存儲哪些字段. Defaults to 'undefined' ,默認(rèn)User所有字段都存儲.
usernameLowerCase: 是否將username字段小寫處理. Defaults to 'false'.
populateFields:findByUsername方法返回的字段. Defaults to 'undefined'.全都返回
encoding: salt編碼. Defaults to 'hex'.
limitAttempts: 是否限制登陸嘗試次數(shù). Default: false.
maxAttempts: 最大嘗試次數(shù). Default: Infinity.
passwordValidator:定義password驗證方法, 'function(password,cb)'. Default: 非空驗證
usernameQueryFields: 定義額外的用戶鑒別字段 (e.g. email).
findByUsername: Specifies a query function that is executed with query parameters to restrict the query with extra query parameters. For example query only users with field "active" set to true. Default: function(model, queryParameters) { return model.findOne(queryParameters); }. See the examples section for a use case.
6.2 config:
//引入前面定義好的模型,User上的authenticate()、serializeUser()、deserializeUser()方法是plm自動加上去的靜態(tài)方法
const User = require('./models/user');
// > 0.2.1版本可以這樣寫:passport.use(User.createStrategy());
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
6.3 plm自動加入User上的實例方法和靜態(tài)方法(實例、靜態(tài)的含義和mongoose一樣,前者作用在實例上,后者作用在類上):
實例方法:
setPassword(password, cb): 根據(jù)password異步生成hash和salt
changePassword(oldPassword, newPassword, cb): 修改密碼
authenticate(password, cb): 驗證
resetAttempts(cb): 重置錯誤次數(shù)(whenoptions.limitAttempts=true)
靜態(tài)方法:
authenticate()、serializeUser()、deserializeUser()、createStrategy(): 在Passport's LocalStrategy中使用
register(user, password, cb):是一個方便的注冊方法,自動檢查username是否為空,并自動對密碼進行hash和加鹽
findByUsername(): 方便的根據(jù)唯一用戶名查找方法
7. 來個栗子
綜上所述,我們使用express@4+passport+mongoose試驗一下,首先說明,我的node版本是8.9.3, 對ES6語法有限支持,示例會使用ES6語法。
node -v
v8.9.3
7.1 我們不使用express-generator,自己從頭建立工程,以便對整個流程更加清晰:
mkdir express-passport-test
cd express-passport-test
npm init
7.2 首先確定一下需要安裝的依賴:
"dependencies": {
"body-parser": "^1.18.2",
"cookie-parser": "^1.4.3",
"express": "^4.16.2",
"express-session": "^1.15.6",
"mongoose": "^5.0.3",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^4.4.0"
}
既然使用cookie和session驗證,那么body-parser和cookie-parser、express-session自然必不可少
7.3 項目結(jié)構(gòu):

個人有點代碼潔癖,覺得官方生成的模板太亂了,就這樣整理一下
7.4 let's code
- models/user.js(M)
/*
* @Author: AngelaDaddy
* @Date: 2018-02-03 13:20:04
* @Last Modified by: AngelaDaddy
* @Last Modified time: 2018-02-03 13:46:49
* @Description: User領(lǐng)域類
* 由于passport和user類緊密結(jié)合,所以直接寫在一起好點
* 否則應(yīng)該分開寫
*/
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const passportLocalMongoose = require('passport-local-mongoose')
const User = new Schema({
username: String,
password: String,
gender: Boolean//可隨意添加字段
});
User.plugin(passportLocalMongoose)
module.exports = mongoose.model('User', User)
- routes/index.js(C)
const express = require('express')
const router = express.Router()
const User = require('../models/user')
module.exports = (app,passport) => {
router.get('/', function (req, res) {
res.render('index', { user: req.user });
})
router.get('/register', function (req, res) {
res.render('register', {})
})
router.post('/register', function (req, res) {
User.register(new User({ username: req.body.username }), req.body.password, function (err, user) {
if (err) {
return res.render('register', { user: user })
}
passport.authenticate('local')(req, res, function () {
res.redirect('/')
})
})
})
router.get('/login', function (req, res) {
res.render('login', { user: req.user })
})
router.post('/login', passport.authenticate('local'), function (req, res) {
res.redirect('/')
})
router.get('/logout', function (req, res) {
req.logout()
res.redirect('/')
});
app.use('/',router)
}
- utils/
- db.js 負(fù)責(zé)數(shù)據(jù)庫連接
const mongoose = require('mongoose')
const conn = ()=>{
mongoose.connect('mongodb://localhost/express4_passport');
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function () {
console.log('db connection success!')
});
}
module.exports = {
conn:conn
}
- errorHandler.js:錯誤處理
module.exports = (app) => {
app.use(function(req, res, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
})
// 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: {}
})
})
}
- passport.js: passport定義及實現(xiàn)
const passport = require('passport')
//使用passport本地策略
const LocalStrategy = require('passport-local').Strategy
const User = require('../models/user')
module.exports = (app) => {
passport.use(new LocalStrategy(User.authenticate()))
passport.serializeUser(User.serializeUser())
passport.deserializeUser(User.deserializeUser())
//使用express session
app.use(require('express-session')({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false
}))
//啟用passport
app.use(passport.initialize())
//使用session驗證
app.use(passport.session())
//由于passport在其它租間還要使用(router),將其返回
return passport
}
- views:
//layout
doctype html
html
head
title= title
meta(name='viewport', content='width=device-width, initial-scale=1.0')
link(, rel='stylesheet', media='screen')
body
block content
//index
extends layout
block content
if (!user)
a(href="/login") Login
br
a(href="/register") Register
if (user)
p You are currently logged in as #{user.username}
a(href="/logout") Logout
//- login
extends layout
block content
.container
h1 Login Page
p.lead Say something worthwhile here.
br
form(role='form', action="/login",method="post", style='max-width: 300px;')
.form-group
input.form-control(type='text', name="username", placeholder='Enter Username')
.form-group
input.form-control(type='password', name="password", placeholder='Password')
button.btn.btn-default(type='submit') Submit
a(href='/')
button.btn.btn-primary(type="button") Cancel
// -register
extends layout
block content
.container
h1 Register Page
p.lead Say something worthwhile here.
br
form(role='form', action="/register",method="post", style='max-width: 300px;')
.form-group
input.form-control(type='text', name="username", placeholder='Enter Username')
.form-group
input.form-control(type='password', name="password", placeholder='Password')
button.btn.btn-default(type='submit') Submit
a(href='/')
button.btn.btn-primary(type="button") Cancel
//- error
extends layout
block content
if (message)
p #{message}
if (error)
p #{error}
最后,在appStarter.js中隊所有事情進行綜合:
/*
* @Author: AngelaDaddy
* @Date: 2018-02-03 13:35:36
* @Last Modified by: AngelaDaddy
* @Last Modified time: 2018-02-03 13:56:55
* @Description: middlware?????€??’?”?
*/
const path = require('path')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const db = require('./utilis/db')
const errorHandler = require('./utilis/errorHandler')
const passport = require('./utilis/passport')
const router = require('./routes')
module.exports = (app) => {
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
db.conn()
router(app,passport(app))
errorHandler(app)
const server = app.listen(3000, function () {
const host = server.address().address
const port = server.address().port
console.log('Example app listening at http://%s:%s', host, port)
})
}
然后,我得到了一個幾乎什么都沒有的項目啟動文件:
/*
* @Author: AngelaDaddy
* @Date: 2018-02-03 13:18:35
* @Last Modified by: AngelaDaddy
* @Last Modified time: 2018-02-03 13:39:17
* @Description: 程序入口文件
*/
const express = require('express')
const appStarter = require('./appStarter')
const app = express()
appStarter(app)
8. 啟動測試:




項目github地址
至此,本教程全部完成,寫的好累啊~~~手都酸了,點個贊再走唄!