2016年3月25日 星期五

Node JS + Express JS + Passport JS 做一個登入系統 & 多個帳號綁定的範例

教學出處

安裝所需要的模組


  1. Express
  2. mongoose
  3. passport
  4. passport-facebook
  5. connect-flash
  6. bycrypt-nodejs
  7. morgan
  8. body-parser
  9. cookie-parser
  10. 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

// 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);
view raw server.js hosted with ❤ by GitHub

DB設定 config/database.js

/*
依照自己的環境填入URL
*/
module.exports = {
//協定是mongodb, ip: 你主機的IP位置, port:預設是27017, userDb是資料表名稱, 隨你取
url: 'mongodb://localhost:27017/userDb'
}
view raw database.js hosted with ❤ by GitHub


路由 app/routes.js

我們用以下的路由來試範
  • 首頁 : /
  • 登入頁 : /login
  • 註冊頁 : /signup
  • 處理登入 (POST)
  • 處理註冊 (POST)
  • 個人檔案: /profile
// 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('/');
}
view raw routes.js hosted with ❤ by GitHub

User model 

// 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);
view raw user.js hosted with ❤ by GitHub
// 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);
view raw user.js hosted with ❤ by GitHub


Passport.js

// 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);
});
}));
};
view raw passport.js hosted with ❤ by GitHub