nodeJS Code Gen Party 10 - 为 ToDo List 增加第三方认证
这次 nodeJS Taiwan 社群 code gen party 10 活动,要接续之前 code gen party 8 活动 Ben 做的 ToDo List Sample,增加第三方认证 OAuth 功能。
这次的实作以 everyauth 为主,增加基本的使用者帐号密码验证功能,再扩充到使用 Facebook 的 OAuth 认证。
程式码:https://github.com/jacksctsai/authentication-todo-example
下载:https://github.com/jacksctsai/authentication-todo-example/downloads
步骤
自 Github clone express-todo-example 并执行
取回程式码
git clone https://github.com/dreamerslab/express-todo-example.git
cd express-todo-example
安装相依模组(照 package.json 定义)
npm install
启动 server
node app.js
Express server listening on port 3001 in development mode
开启浏览器,确认 server 顺利执行
open http://localhost:3001
安装认证所需 npm modules
npm install everyauth
记录安装的 everyauth 版本号,这次刚好是 0.2.32。
修改 package.json,增加 everyauth 模组
vim package.json
依照安装的 everyauth 版本,修改 package.json
{
"name" : "todo",
"version" : "0.0.2",
"private" : true,
"dependencies" : {
"connect" : "1.8.7",
"express" : "2.5.9",
"ejs" : ">= 0.0.1",
"mongoose" : "2.6.7",
"everyauth" : "0.2.32"
}
}
实作帐号密码登入
增加 User mongoose data model
修改 db.js 增加以下程式码
var User = new Schema({ id : { type : String, unique : true }, password : String }); mongoose.model( 'User', User );
新增 auth.js,程式码如下
var everyauth = require('everyauth'); var mongoose = require( 'mongoose' ); var User = mongoose.model( 'User' ); everyauth.everymodule.findUserById( function (userId, callback) { User. findOne({ id : userId }). run( callback ); }); everyauth.password .getLoginPath('/login') // Login page url .postLoginPath('/login') // Url that your login form POSTs to .loginView('login') .authenticate( function (login, password) { var promise = this.Promise(); User. findOne({ id : login , password : password }). run( function ( err, user ){ if ( !user ) { err = 'Invalid login'; } if( err ) return promise.fulfill( [ err ] ); promise.fulfill( user ); }); return promise; }) .loginSuccessRedirect('/') // Where to redirect to after login .getRegisterPath('/signup') // Registration url .postRegisterPath('/signup') // Url that your registration form POSTs to .registerView('signup') .validateRegistration( function (newUser) { if (!newUser.login || !newUser.password) { return ['Either ID or Password is missing.']; } return null; }) .registerUser( function (newUser) { var promise = this.Promise(); new User({ id : newUser.login, password : newUser.password }).save( function ( err, user, count ){ if( err ) return promise.fulfill( [ err ] ); promise.fulfill( user ); }); return promise; }) .registerSuccessRedirect('/') // Url to redirect to after a successful registration .loginLocals( {title: 'Login'}) .registerLocals( {title: 'Sign up'}); module.exports = { requireLogin: function( req, res, next ) { if (!req.loggedIn) { res.redirect( '/' ); return; } next(); } };
修改 app.js
/** * Module dependencies. */ var express = require( 'express' ); var everyauth = require('everyauth'); var app = module.exports = express.createServer(); // mongoose setup require( './db' ); // autoentication setup var auth = require( './auth' ); // add everyauth view helpers to express everyauth.helpExpress( app ); var routes = require( './routes' ); // Configuration app.configure( 'development', function (){ app.set( 'views', __dirname + '/views' ); app.set( 'view engine', 'ejs' ); app.use( express.favicon()); app.use( express.static( __dirname + '/public' )); app.use( express.logger()); app.use( express.cookieParser()); app.use( express.bodyParser()); //app.use( routes.current_user ); app.use( express.session({secret: 'nodeTWParty'}) ); app.use( everyauth.middleware() ); app.use( app.router ); app.use( express.errorHandler({ dumpExceptions : true, showStack : true })); }); app.configure( 'production', function (){ app.set( 'views', __dirname + '/views' ); app.set( 'view engine', 'ejs' ); app.use( express.cookieParser()); app.use( express.bodyParser()); //app.use( routes.current_user ); app.use( express.session({secret: 'nodeTWParty'}) ); app.use( everyauth.middleware() ); app.use( app.router ); app.use( express.errorHandler()); }); // Routes app.get( '/', routes.index ); app.post( '/create', auth.requireLogin, routes.create ); app.get( '/destroy/:id', auth.requireLogin, routes.destroy ); app.get( '/edit/:id', auth.requireLogin, routes.edit ); app.post( '/update/:id', auth.requireLogin, routes.update ); app.listen( 3001, '127.0.0.1', function (){ console.log( 'Express server listening on port %d in %s mode', app.address().port, app.settings.env ); });
修改 views/index.ejs 如下
<h1><%= title %></h1>
<% if (everyauth.loggedIn) { %>
<div>
<center>
Hi, <%= user.id %>. Good to see you! <a href="/logout">Log out</a>
</center>
</div>
<div>
<form action="/create" method="post" accept-charset="utf-8">
<div class="item-new">
<input class="input" type="text" name="content" />
</div>
</form>
<% todos.forEach( function ( todo ){ %>
<div class="item">
<a class="update-link" href="/edit/<%= todo._id %>" title="Update this todo item"><%= todo.content %></a>
<a class="del-btn" href="/destroy/<%= todo._id %>" title="Delete this todo item">Delete</a>
</div>
<% }); %>
</div>
<% } else { %>
<div>
<center>
You are not logged in. <br>
Please <a href="/login">login</aor <a href="/signup">sign up</a>.
</center>
</div>
<% } %>
在 views 目录下增加 signup.ejs 档案
<h1>Sign up</h1>
<% if( typeof(errors) !== 'undefined' ) { %>
<center>
<%= errors %>
</center>
<% } %>
<div>
<form action="/signup" method="post" accept-charset="utf-8">
<div class="item-new">
ID <input class="input" type="text" name="login" />
</div>
<div class="item-new">
Password <input class="input" type="password" name="password" />
</div>
<div class="item-new">
<input type="submit" value="Submit" />
</div>
</form>
</div>
在 views 目录下增加 login.ejs 档案
<h1>Login</h1>
<% if( typeof(errors) !== 'undefined' ) { %>
<center>
<%= errors %>
</center>
<% } %>
<div>
<form action="/login" method="post" accept-charset="utf-8">
<div class="item-new">
ID <input class="input" type="text" name="login" />
</div>
<div class="item-new">
Password <input class="input" type="password" name="password" />
</div>
<div class="item-new">
<input type="submit" value="Login" />
</div>
</form>
</div>
修改 routes/index.js 档案
var mongoose = require( 'mongoose' ); var Todo = mongoose.model( 'Todo' ); var utils = require( 'connect' ).utils; var everyauth= require( 'everyauth' ); exports.index = function ( req, res, next ){ if (!req.loggedIn) { res.render( 'index', { title : 'Express Todo Example', todos : [] }); return; }
还要把档案中的 req.cookies.user_id 置换成 req.user.id
到这裡就己经完成网站初步的使用者认证机制了。
建立 Facebook Application
到 https://developers.facebook.com/apps 建立一个应用程式(名称无所谓),
App Domains 跟 网站位址(URL)都填:http://local.host:3001/
设定 local.host 网域
Facebook 不接受 localhost 的网域名称,所以为了要测试,我们改用另一个网域名称: local.host
要能用这个测试网域名称,在 Linux / Mac OS X 底下可以
修改 /etc/hosts 档案来做
sudo vim /etc/hosts
参考下面内容修改或增加一行
127.0.0.1 localhost local.host
如果是 Windows,则要修改 C:\WINDOWS\system32\drivers\etc\hosts
档案
127.0.0.1 local.host
实作 Facebook 登入
修改 db.js 档案
// 由于 id 是 Facebook 产生的一个代码, // 所以我们加一个栏位 name 当做使用者名称 var User = new Schema({ id : { type : String, unique : true }, name : String, profile : String, password : String });
修改 auth.js 档案,增加以下内容
everyauth.facebook .appId('AppId') .appSecret('App Secret') .handleAuthCallbackError( function (req, res) { res.redirect('/'); }) .findOrCreateUser( function (session, accessToken, accessTokExtra, fbUserMetadata) { var promise = this.Promise(); User.findOne({ id : fbUserMetadata.id }).run( function( err, user ){ if( err ) return promise.fulfill( [ err ] ); if( user ) { promise.fulfill( user ); } else { new User({ id : fbUserMetadata.id, name : fbUserMetadata.name, profile : fbUserMetadata }).save( function ( err, user, count ){ if( err ) return promise.fulfill( [ err ] ); promise.fulfill( user ); }); } }); return promise; }) .redirectPath('/');
修改 views/index.ejs 档案
<% if (everyauth.loggedIn) { %>
<div>
<center>
Hi, <%= user.name || user.id %>. Good to see you! <a href="/logout">Log out</a>
</center>
</div>
...
<% } else { %>
...
Login with <a href="/auth/facebook">Facebook</a>....
<% } %>
测试 Facebook 登入
重新以 http://local.host:3001/ 网址进入网站测试
后记
今天 Ben 在活动结束的时候有提到 password.js。Everyauth 虽然很多人用,照著他的说明也很容易上手,但不可否认的 everyauth 与 express.js 的依赖太深,以致程式码会有点乱。
针对这个问题 password.js 提供了一个比较乾淨的做法,可以自由的跟其他 web framework 搭配。而且由于 passport.js 切的比较乾淨,未来在增加新的认证提供者(authentication provider)时,要改的程式码也比较少(因为大部份的认证程式码都放在独立的 passport.js strategy 裡了)。
建议对于认证有兴趣的朋友可以再研究一下 passport.js。AiNiOOO 原来也是从 everyauth 开始,但后来就改成 passport.js 了。