nodeJS Code Gen Party 10 - 为 ToDo List 增加第三方认证

优质
小牛编辑
118浏览
2023-12-01

这次 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 了。