Help us understand the problem. What is going on with this article?

Express.jsとBookshelf.jsでユーザ認証

More than 5 years have passed since last update.

Node.jsにおいてExpressはWebアプリケーション作成時のほぼデファクトスタンダードですが、こんかいはさらにORMにBookshelfをもちいた場合のユーザ認証です。BookshelfはBackboneを下敷きにプロミスインタフェースを提供するRDB向けORMです。MySQL、PostgreSQL、Sqlite3をサポートしています。バリデーションロジック等をもっておらずシンプルなことが特徴です。

ユーザ認証は多くのアプリケーションにとって必須の機能です。今回は単純なユーザ認証付きのブログアプリケーションを作成してみます。作成したアプリケーションは以下リポジトリにおいて有ります:

https://bitbucket.org/p_baleine/express-bookshelf-user-auth

ルーティングは以下の通り:

  • / 投稿一覧
  • /posts 投稿一覧
  • /admin/sessions/new ログイン画面
  • /admin/posts 管理者向け投稿一覧(ログインしていないとみれない)

アプリケーションの作成

expressコマンドでアプリケーションを作成します、

$ npm install -g express # まだ`express`をインストールしていない場合のみ
$ express -s express-bookshelf-user-auth
$ cd express-bookshelf-user-auth && npm install

ついでにUnderscoreもインストールしておきます、

$ npm install underscore --save

ルーティング

RESTfulなルーティングの定義をするためにexpress-resourceを導入します、

$ npm install express-resource --save

app.jsを以下の通り変更します:

/**
 * Module dependencies.
 */

var express = require('express')
  , _ = require('underscore') // ← 追加
  , posts = require('./routes/posts') // ← 追加
  , admin = require('./admin') // ← 追加
  , http = require('http')
  , path = require('path')
  , Resource = require('express-resource'); // ← 追加

var app = module.exports = express(); // ← 変更

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser('your secret here'));
  app.use(express.session());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function(){
  app.use(express.errorHandler());
});

// ↓変更

app.get('/', posts.index);

app.resource('posts', _.pick(posts, 'index', 'show')); // 未ログインだと`index`と`show`のみアクセス可

// adminサブアプリをマウント
app.use('/admin', admin);

if (!module.parent) {
  app.listen(app.get('port'));
  console.log("Express server listening on port " + app.get('port'));
}

ユーザ認証はadminサブアプリに切り出します、

$ mkdir admin
$ touch admin/index.js

admin/index.jsに以下を記載します、

/**
 * Module dependencies.
 */

var express = require('express')
  , posts = require('../routes/posts')
  , sessions = require('../routes/sessions')
  , http = require('http')
  , path = require('path')
  , Resource = require('express-resource')
  , User = require('../models/user');

var app = module.exports = express();

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/../views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser('your secret here'));
  app.use(express.session());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function(){
  app.use(express.errorHandler());
});

app.resource('posts', posts);
app.resource('sessions', sessions);

routes/posts.jsに以下のダミーアクションを記述しておきます

exports.index = function(req, res) {
  res.send('投稿一覧、' + (req.user ? 'ログイン済' : '未ログイン'));
};

exports.show = function(req, res) {
  res.send('投稿 ' + req.params.post + 'の詳細');
};

exports.new = function(req, res) {
  res.send('新規投稿作成画面');
};

exports.cretate = function(req, res) {
  res.send('新規投稿作成');
};

exports.edit = function(req, res) {
  res.send('投稿 ' + req.params.post + 'の編集画面');
};

exports.update = function(req, res) {
  res.send('投稿 ' + req.params.post + 'の更新');
};

exports.del = function(req, res) {
  res.send('投稿 ' + req.params.post + 'の削除');
};

ログイン画面の実装

routes/sessions.jsにログイン画面のルーティングを記述します、

exports.new = function(req, res) {
  res.render('session/new');
};

views/session/new.jadeを作成します、

$ mkdir views/session
$ touch views/session/new.jade
extends ../layout

block content
  h1 ログイン
  if error
    #error= e
  form(action="/admin/sessions" method="post")
    .form-group
      label(for="user[email]")
      input(type="email" name="user[email]" plaeholder="Enter your email")
    .form-group
      label(for="user[password]")
      input(type="password" name="user[password]")
    input(type="submit" value="ログイン")

ためしに以下コマンドでサーバを立ち上げてhttp://localhost:3000/admin/sessions/newにアクセスできることを確認します。

$ npm start

ログインの実装

routes/sessions.jsにログインのルーティングを記述します、Userモデルのauthenticateメソッドで認証できた場合のみ/admin/postsにリダイレクトします、

var User = require('../models/user');

exports.create = function(req, res) {
  var user = req.body.user;

  User.authenticate(user.email, user.password).then(function(user) {
    req.session.uid = user.id;
    res.redirect('/posts');
  }, function(e) {
    req.session.uid = null;
    res.statusCode = 404;
    res.render('session/new', { error: e.message });
  });
};

Userモデルの実装

必要になるモジュールをインストールしておきます、

$ npm install bookshelf sqlite3 knex bluebird bcrypt --save

データベースの設定をconfig.jsに記述します、これはアプリケーションでデータベースに接続する時とマイグレーション実行時に利用します、

module.exports = {
  "directory": "./db/migrations",
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "./db/blog-dev.db"
    },
    "debug": true
  }
};

以下コマンドでマイグレーションのファイルを作成します、

$ mkdir db
$ ./node_modules/.bin/knex migrate:make create_user -c ./config.js

作成されたマイグレーションファイルdb/migrations/<タイムスタンプ>_create_user.jsを編集します、

exports.up = function(knex, Promise) {
  return knex.schema.createTable('users', function(t) {
    t.increments().primary();
    t.string('email').notNull();
    t.string('password').notNull();
    t.string('salt').notNull();
    t.dateTime('created_at').notNull();
    t.dateTime('updated_at').nullable();
  });  
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTable('users');  
};

マイグレーションを実行します、

$ ./node_modules/.bin/knex migrate:latest -c ./config.js

Bookshelfはプロミスインタフェースを提供しているので、自前のメソッドもやはりプリミスベースで記述すると利用しやすいです。models/user.jsを以下の通り作成します。ユーザの新規作成時にhashPasswordメソッドでパスワードを暗号化します。authenticateスタティックメソッドでは与えられたemailからユーザをselectして、パスワードの比較をします。

var Promise = require('bluebird'),
    bcrypt = Promise.promisifyAll(require('bcrypt')),
    Bookshelf = require('bookshelf'),

    // database setting
    config = require('../config'),

    // initialize database
    blogBookshelf = Bookshelf.blogBookshelf = Bookshelf.initialize(config.database);

/**
 * User model
 */

var User = module.exports = blogBookshelf.Model.extend({

  tableName: 'users',

  hasTimestamps: true,

  initialize: function() {
    blogBookshelf.Model.prototype.initialize.apply(this, arguments);
    this.on('saving', this.hashPassword, this);
  },

  /**
   * Set salted password.
   */

  hashPassword: function() {
    var _this = this;

    return bcrypt.genSaltAsync(10)
      .then(function(salt) {
        _this.set('salt', salt);
        return bcrypt.hashAsync(_this.get('password'), salt);
      })
      .then(function(hash) {
        return _this.set('password', hash);
      });
  }

}, {

  /**
   * Authenticate user.
   *
   * @param {String} email
   * @param {String} password
   * @return {Promise}
   */

  authenticate: function(email, password) {
    return new this({ email: email }).fetch({ require: true })
      .then(function(user) {
        return [bcrypt.hashAsync(password, user.get('salt')), user];
      })
      .spread(function(hash, user) {
        if (hash === user.get('password')) { return user; }
        throw new Error('password or email is incorrect.');
      });
  }

});

テスト用のユーザをreplから作成します、

$ node
> var User = require('./models/user');
> new User({ email: 'p.baleine@gmail.com', password: 'mypassword' }).save().then(function(user) {
  console.log(user);
  });

サーバを立ち上げて/admin/sessions/newにアクセスし先程作成したユーザのメールアドレスとパスワードを入力すると投稿一覧に遷移できます。

アクセス制限

最後に管理者権限のない人のアクセスを制限します、admin/index.jsに以下を記述します、

var User = require('../models/user');

app.all(/^\/posts/, restrict, loadUser);

app.resource('posts', posts);
app.resource('sessions', sessions);

function restrict(req, res, next) {
  if (!req.session.uid) { return res.redirect('/'); }
  next();
}

function loadUser(req, res, next) {
  if (!req.session.uid) { return next(); }
  new User({ id: req.session.uid }).fetch().then(function(user) {
    req.user = user;
    next();
  });
}

ログインしていない人が/admin/postsで始まるURLにアクセスするとトップにリダイレクトされます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away