安裝所需要的模組
- Express
- mongoose
- passport
- passport-facebook
- connect-flash
- bycrypt-nodejs
- morgan
- body-parser
- cookie-parser
- express-session
以上都用npm安裝, 沒接觸過的請自行GOOGLE
建立檔案的結構
- app
------ routes.js //處理路由
-models //所有DB的操作都放在這裡
------ user.js
- config //設定檔都放這
------ auth.js //處理串接社群API的設定(ClientId, Secret, callbackUrl...etc)
------ database.js //設定DB連線的參數
------ passport.js //設定passport的認證策略
- views
------ index.ejs //起始畫面, 選擇登入的方式
------ login.ejs //登入畫面
------ signup.ejs //註冊畫面
------ profile.ejs //登入後顯示個人檔案
- server.js //沒什麼好解釋的, 就是SERVER
Server.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server.js | |
// 把所需的模組載入 | |
var express = require('express'); | |
var app = express(); | |
var port = process.env.PORT || 8080; | |
var mongoose = require('mongoose'); | |
var passport = require('passport'); | |
var flash = require('connect-flash'); | |
var morgan = require('morgan'); | |
var cookieParser = require('cookie-parser'); | |
var bodyParser = require('body-parser'); | |
var session = require('express-session'); | |
// 設定 =============================================================== | |
var configDB = require('./config/database.js'); | |
mongoose.connect(configDB.url); | |
require('./config/passport')(passport); | |
// 套入 express 的應用程式 | |
app.use(morgan('dev')); // 把每個請求都顯示在 console | |
app.use(cookieParser()); // 認證需要用到 | |
app.use(bodyParser()); // 讀取 html 表格的資料(POST...etc) | |
app.set('view engine', 'ejs'); // 設定 ejs 為套用模版的引擎 | |
// 啟用 passport | |
app.use(session({ secret: 'blablablablablablablablablablabla' })); // session 的加密密鑰 | |
app.use(passport.initialize()); | |
app.use(passport.session()); // 紀錄 session | |
app.use(flash()); // 回饋訊息處理 | |
// 路由 ====================================================================== | |
require('./app/routes.js')(app, passport); // load our routes and pass in our app and fully configured passport | |
// 啟動server ====================================================================== | |
app.listen(port); | |
console.log('Listening on port ' + port); |
DB設定 config/database.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
依照自己的環境填入URL | |
*/ | |
module.exports = { | |
//協定是mongodb, ip: 你主機的IP位置, port:預設是27017, userDb是資料表名稱, 隨你取 | |
url: 'mongodb://localhost:27017/userDb' | |
} |
路由 app/routes.js
我們用以下的路由來試範
- 首頁 : /
- 登入頁 : /login
- 註冊頁 : /signup
- 處理登入 (POST)
- 處理註冊 (POST)
- 個人檔案: /profile
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/routes.js | |
module.exports = function(app, passport) { | |
// 首頁 =============================== | |
app.get('/', function(req, res) { | |
res.render('index.ejs'); // 載入 index.ejs file | |
}); | |
// 登入頁 | |
app.get('/login', function(req, res) { | |
res.render('login.ejs', { message: req.flash('loginMessage') }); | |
}); | |
// 處理登入 | |
app.post('/login', passport.authenticate('local-login', { | |
successRedirect : '/profile', // 成功則導入profile | |
failureRedirect : '/login', // 失敗則返回登入頁 | |
failureFlash : true // 允許 flash 訊息 | |
})); | |
// FACEBOOK 登入路由 ===================== | |
app.get('/auth/facebook', passport.authenticate('facebook')); | |
// 處理登入後的callback url | |
app.get('/auth/facebook/callback', | |
passport.authenticate('facebook', { | |
successRedirect : '/profile', | |
failureRedirect : '/' | |
})); | |
// 登出用 | |
app.get('/logout', function(req, res) { | |
req.logout(); | |
res.redirect('/'); | |
}); | |
// 帳號綁定/聯結其他社群帳號 ============= | |
// 綁定本地帳戶 -------------------------------- | |
app.get('/connect/local', isLoggedIn, function(req, res) { | |
res.render('connect-local.ejs', { message: req.flash('loginMessage') }); | |
}); | |
app.post('/connect/local', isLoggedIn, passport.authenticate('local-connect', { | |
successRedirect : '/profile', // redirect to the secure profile section | |
failureRedirect : '/connect/local', // redirect back to the signup page if there is an error | |
failureFlash : true // allow flash messages | |
})); | |
// facebook 綁定------------------------------- | |
app.get('/connect/facebook', passport.authorize('facebook', { scope : 'email' })); | |
app.get('/connect/facebook/callback', | |
passport.authorize('facebook', { | |
successRedirect : '/profile', | |
failureRedirect : '/' | |
})); | |
// 註冊表單 | |
app.get('/signup', function(req, res) { | |
res.render('signup.ejs', { message: req.flash('signupMessage') }); | |
}); | |
// 處理註冊 | |
app.post('/signup', passport.authenticate('local-signup', { | |
successRedirect : '/profile', // redirect to the secure profile section | |
failureRedirect : '/signup', // redirect back to the signup page if there is an error | |
failureFlash : true // allow flash messages | |
})); | |
// PROFILE ===================== | |
// 需要權限才能造訪的頁面我們就用 isLoggedIn function 來處理 | |
app.get('/profile', isLoggedIn, function(req, res) { | |
res.render('profile.ejs', { | |
user : req.user | |
}); | |
}); | |
// 帳號解除綁定 ============================================================= | |
// 社群帳號, 只移除token以方便日後要重新綁定 | |
// 本地帳號則會移除email & password | |
// 本地帳號 ----------------------------------- | |
app.get('/unlink/local', function(req, res) { | |
var user = req.user; | |
user.local.email = undefined; | |
user.local.password = undefined; | |
user.save(function(err) { | |
res.redirect('/profile'); | |
}); | |
}); | |
// facebook ------------------------------- | |
app.get('/unlink/facebook', function(req, res) { | |
var user = req.user; | |
user.facebook.token = undefined; | |
user.save(function(err) { | |
res.redirect('/profile'); | |
}); | |
}); | |
// 登出 ============================== | |
app.get('/logout', function(req, res) { | |
req.logout(); | |
res.redirect('/'); | |
}); | |
}; | |
// 處理權限 | |
function isLoggedIn(req, res, next) { | |
if (req.isAuthenticated()) | |
return next(); | |
res.redirect('/'); | |
} |
User model
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/models/user.js | |
// 載入需要的東西 | |
var mongoose = require('mongoose'); | |
var bcrypt = require('bcrypt-nodejs'); | |
// 定義欄位 | |
var userSchema = mongoose.Schema({ | |
local : { | |
email : String, | |
password : String, | |
}, | |
facebook : { | |
id : String, | |
token : String, | |
email : String, | |
name : String | |
} | |
}); | |
// methods ====================== | |
// 加密 | |
userSchema.methods.generateHash = function(password) { | |
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); | |
}; | |
// 核對密碼 | |
userSchema.methods.validPassword = function(password) { | |
return bcrypt.compareSync(password, this.local.password); | |
}; | |
module.exports = mongoose.model('User', userSchema); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/models/user.js | |
// 載入需要的東西 | |
var mongoose = require('mongoose'); | |
var bcrypt = require('bcrypt-nodejs'); | |
// 定義欄位 | |
var userSchema = mongoose.Schema({ | |
local : { | |
email : String, | |
password : String, | |
}, | |
facebook : { | |
id : String, | |
token : String, | |
email : String, | |
name : String | |
} | |
}); | |
// methods ====================== | |
// 加密 | |
userSchema.methods.generateHash = function(password) { | |
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); | |
}; | |
// 核對密碼 | |
userSchema.methods.validPassword = function(password) { | |
return bcrypt.compareSync(password, this.local.password); | |
}; | |
module.exports = mongoose.model('User', userSchema); |
Passport.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// config/passport.js | |
var LocalStrategy = require('passport-local').Strategy; | |
var FacebookStrategy = require('passport-facebook').Strategy; | |
var User = require('../models/user'); | |
var configAuth = require('./auth'); | |
module.exports = function(passport) { | |
// passport session setup ================================================== | |
// required for persistent login sessions | |
// passport needs ability to serialize and unserialize users out of session | |
// used to serialize the user for the session | |
passport.serializeUser(function(user, done) { | |
done(null, user.id); | |
}); | |
// used to deserialize the user | |
passport.deserializeUser(function(id, done) { | |
User.findById(id, function(err, user) { | |
done(err, user); | |
}); | |
}); | |
// FACEBOOK ================================================================ | |
passport.use(new FacebookStrategy({ | |
// pull in our app id and secret from our auth.js file | |
clientID : configAuth.facebookAuth.clientID, | |
clientSecret : configAuth.facebookAuth.clientSecret, | |
callbackURL : configAuth.facebookAuth.callbackURL, | |
passReqToCallback : true, | |
profileFields :['id', 'name', 'emails'] | |
}, | |
// 處理 facebook 回傳的 token & 個人資料 | |
function(req, token, refreshToken, profile, done) { | |
process.nextTick(function() { | |
if(!req.user){ | |
// 在 DB 找 facebook id | |
User.findOne({ 'facebook.id' : profile.id }, function(err, user) { | |
if (err) | |
return done(err); | |
if (user) { | |
if (!user.facebook.token) { | |
user.facebook.token = token; | |
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; | |
user.facebook.email = profile.emails[0].value; | |
user.save(function(err) { | |
if (err) | |
throw err; | |
return done(null, user); | |
}); | |
} | |
return done(null, user); // user found, return that user | |
} else { | |
// 如果沒有該USER則新增 | |
var newUser = new User(); | |
// set all of the facebook information in our user model | |
newUser.facebook.id = profile.id; | |
newUser.facebook.token = token; | |
newUser.facebook.name = profile.name.familyName + ' ' + profile.name.givenName; | |
newUser.facebook.email = profile.emails[0].value; | |
newUser.save(function(err) { | |
if (err) | |
throw err; | |
return done(null, newUser); | |
}); | |
} | |
}); | |
}else{ | |
// user already exists and is logged in, we have to link accounts | |
var user = req.user; // pull the user out of the session | |
// update the current users facebook credentials | |
user.facebook.id = profile.id; | |
user.facebook.token = token; | |
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName; | |
user.facebook.email = profile.emails[0].value; | |
// save the user | |
user.save(function(err) { | |
if (err) | |
throw err; | |
return done(null, user); | |
}); | |
} | |
}); | |
})); | |
// LOCAL 註冊 ============================================================ | |
passport.use('local-signup', new LocalStrategy({ | |
usernameField : 'email', | |
passwordField : 'password', | |
passReqToCallback : true | |
}, | |
function(req, email, password, done) { | |
process.nextTick(function() { | |
User.findOne({'local.email' : email}, function(err, user) { | |
if (err) | |
return done(err); | |
if (user) { | |
return done(null, false, req.flash('signupMessage', 'That email is already taken.')); | |
} else { | |
var newUser = new User(); | |
newUser.local.email = email; | |
newUser.local.password = newUser.generateHash(password); | |
newUser.save(function(err) { | |
if (err) | |
throw err; | |
return done(null, newUser); | |
}); | |
} | |
}); | |
}); | |
})); | |
// LOCAL 帳號綁定 ============================================================ | |
passport.use('local-connect', new LocalStrategy({ | |
usernameField : 'email', | |
passwordField : 'password', | |
passReqToCallback : true | |
}, | |
function(req, email, password, done) { | |
process.nextTick(function() { | |
User.findOne({$or :[{'local.email' : email}, {'facebook.email' : email}]}, function(err, user) { | |
if (err) | |
return done(err); | |
if (user && user.local.email == email) { | |
return done(null, false, req.flash('signupMessage', 'That email is already taken.')); | |
} else { | |
var connect_user; | |
if(user.facebook.email == email){ | |
connect_user = user; | |
}else{ | |
connect_user = new User(); | |
} | |
connect_user.local.email = email; | |
connect_user.local.password = connect_user.generateHash(password); | |
connect_user.save(function(err) { | |
if (err) throw err; | |
return done(null, connect_user); | |
}); | |
} | |
}); | |
}); | |
})); | |
// ========================================================================= | |
// LOCAL LOGIN ============================================================= | |
// ========================================================================= | |
// we are using named strategies since we have one for login and one for signup | |
// by default, if there was no name, it would just be called 'local' | |
passport.use('local-login', new LocalStrategy({ | |
// by default, local strategy uses username and password, we will override with email | |
usernameField : 'email', | |
passwordField : 'password', | |
passReqToCallback : true // allows us to pass back the entire request to the callback | |
}, | |
function(req, email, password, done) { // callback with email and password from our form | |
// find a user whose email is the same as the forms email | |
// we are checking to see if the user trying to login already exists | |
User.findOne({ 'local.email' : email}, function(err, user) { | |
// if there are any errors, return the error before anything else | |
if (err) | |
return done(err); | |
// if no user is found, return the message | |
if (!user) | |
return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash | |
// if the user is found but the password is wrong | |
if (!user.validPassword(password)) | |
return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata | |
// all is well, return successful user | |
return done(null, user); | |
}); | |
})); | |
}; |