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






2016年3月3日 星期四

PHP Ratchet 的第一個範例 Hello World

上一篇我們已經把 php Ratchet 安裝好了. 現在要來試試教學上的第一個範例 Hello World.

  1. 在安裝目錄(以我的例子是D:\xampp\htdocs\ws)底下建立一個 src資料夾, 在src裡再建立一個MyApp資料夾(名字沒限制, 這裡以MyApp示範), 然後在MyApp裡建立一個管理訊息的應用程式的檔案Chat.php, 這個程式會聆聽4個事件:
    • onOpen - 有新連線的時候會呼叫這個function
    • onMessage - 有新訊息的事件
    • onClose - 連線關閉的事件
    • onError - 連線有錯誤的事件
    <?php
    namespace MyApp;
    use Ratchet\MessageComponentInterface;
    use Ratchet\ConnectionInterface;
    class Chat implements MessageComponentInterface {
    protected $clients;
    public function __construct() {
    $this->clients = new \SplObjectStorage;
    }
    public function onOpen(ConnectionInterface $conn) {
    // Store the new connection to send messages to later
    $this->clients->attach($conn);
    echo "New connection! ({$conn->resourceId})\n";
    }
    public function onMessage(ConnectionInterface $from, $msg) {
    $numRecv = count($this->clients) - 1;
    echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
    , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');
    foreach ($this->clients as $client) {
    if ($from !== $client) {
    // The sender is not the receiver, send to each client connected
    $client->send($msg);
    }
    }
    }
    public function onClose(ConnectionInterface $conn) {
    // The connection is closed, remove it, as we can no longer send it messages
    $this->clients->detach($conn);
    echo "Connection {$conn->resourceId} has disconnected\n";
    }
    public function onError(ConnectionInterface $conn, \Exception $e) {
    echo "An error has occurred: {$e->getMessage()}\n";
    $conn->close();
    }
    }
    view raw chat.php hosted with ❤ by GitHub

  2. 再來, 我們要建立一個處理訊息往來的server, 在安裝目錄底下建立一個bin資料夾然後再建立一個chat-server.php的檔案
    <?php
    use Ratchet\Server\IoServer;
    use Ratchet\Http\HttpServer;
    use Ratchet\WebSocket\WsServer;
    use MyApp\Chat;
    require dirname(__DIR__) . '/vendor/autoload.php';
    $server = IoServer::factory(
    new HttpServer(
    new WsServer(
    new Chat()
    )
    ),
    8080
    );
    $server->run();
    view raw chat-server.php hosted with ❤ by GitHub
  3. 建好chat-server.php之後, 我們在該目錄下用cmd執行
    php chat-server.php
    來啟用"server".
  4. 啟用後, 我們來看看是否有成功執行:
    • 我們在根目錄(xampp是htdocs)建立一個test.html然後把底下的code貼上去
      <!DOCTYPE html>
      <html>
      <head>
      <title>Ratchet Test</title>
      <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
      <script>
      var messages = [];
      // connect to the socket server
      var chat_conn = new WebSocket('ws://localhost:8080');
      chat_conn.onopen = function(e) {
      console.log('Connected to server:', chat_conn);
      }
      chat_conn.onerror = function(e) {
      console.log('Error: Could not connect to server.');
      }
      chat_conn.onclose = function(e) {
      console.log('Connection closed');
      }
      // handle new message received from the socket server
      chat_conn.onmessage = function(e) {
      // message is data property of event object
      var message = JSON.parse(e.data);
      console.log('message', message);
      // add to message list
      var li = '<li>' + message.text + '</li>';
      $('.message-list').append(li);
      }
      // attach onSubmit handler to the form
      $(function() {
      $('.message-form').on('submit', function(e) {
      // prevent form submission which causes page reload
      e.preventDefault();
      // get the input
      var input = $(this).find('input');
      // get message text from the input
      var message = {
      type: 'message',
      text: input.val()
      };
      // clear the input
      input.val('');
      // send message to server
      chat_conn.send(JSON.stringify(message));
      });
      });
      </script>
      </head>
      <body>
      <h1>Chat App Using Ratchet</h1>
      <h2>Messages</h2>
      <ul class="message-list"></ul>
      <form class="message-form">
      <input type="text" size="40" placeholder="Type your message here" />
      <button>Post it!</button>
      </form>
      </body>
      </html>
      view raw test.html hosted with ❤ by GitHub
    • 然後打開2個瀏覽器各自輸入
      localhost/test.html
      127.0.0.1/test.html
    • 如果沒問題, 輸入訊息後就會看到下面的畫面
PS. 如果遇到 "Myapp/Chat not found" 的錯誤訊息, 請先確認composer.json裡的內容是否為
{

 "autoload": {

        "psr-0": {

            "MyApp": "src"

        }

    },

    "require": {

        "cboden/ratchet": "^0.3.5"

    }

}
如果不是, 請依照上面修改, 然後cmd 輸入 composer dumpautoload
重新產生composer.json檔

下一篇要來講講怎麼用Ratchet 做一個push server

在 XAMPP 上安裝 php Ratchet (Windows OS)

這幾天研究了一下用php Ratchet來建立一個websocket server,
發現他的官網給的教學不是很詳細, 在此寫一篇我在安裝流程.

  1. 首先, 要安裝COMPOSER. 對沒用過的人會比較陌生, 不過其實不難, 因為它有WINDOWS版的安裝程式, 下載後點兩下執行就可以了.
  2. 安裝完後可以開始安裝Ratchet, 用COMPOSER安裝需要幾個步驟:
    • 在windows底下, 執行CMD.EXE進入command line模式, 把目錄換到你要安裝的位置(以我的例子是D:\xampp\htdocs\ws)然後輸入
      php composer.phar require cboden/ratchet 注1, 注2
    • 執行完後, 就要開始正式安裝了, 一樣在cmd裡輸入
      php composer.phar install 
    • 等程式跑完就安裝好了Ratchet, 下一篇我會寫實際使用Ratchet做一個hello world 的範例
注1: 教學上是以LINUX為操作環境, 所以會有 ~/ 的符號.
注2: 請確定你有設定環境變數讓 Windows cmd 命令列可以執行 php 指令,這裡有教學