コピペでOK!Expressにログイン機能をつくる

さてさて、前々回の記事「保存版!「sequelize」モデルの使い方実例・全53件」では、データベースの操作方法を網羅した内容をお届けしました。

そして、この記事を書いた結果、データベース関連で1つサイト開発には重要な機能を思いつきました。

それは・・・

ログイン機能

です。

やはり、ある程度の規模のサイトとなるとユーザーがログインして個人的な情報を管理できるようになっていることが多いので、この機能は外せません。

そこで!

今回は「Express」と有名なログイン認証パッケージ「Passport.js」を使ってログイン機能をつくってみたいと思います。

ぜひ皆さんのお役に立てると嬉しいです😊✨

なお、今回は少し内容が複雑な部分も多かったため、asyncawaitを使わず「できるだけシンプル」なコードでログイン機能を実装してみました。(初心者にもやさしい記事づくり、心掛けてます👍)

開発環境: Node.js 8.10、Express 4.1

前提として

データベース操作するパッケージ「Sequelize」がインストールされ、以下2つがすでに作成済みであることが前提です。

  • Usersテーブル( データベース。マイグレーション済み )
  • Userモデル( models/user.js )

テーブルはこうなります。

まだの方は、「Node.jsにDBマイグレーション、Seed、モデルを用意する「Sequelize」」を参考にしてみてください。

また、テンプレートエンジンは「mustache」を使います。

こちらも、「Expressのインストールと基本のまとめ」が参考になると思います。

必要なパッケージをインストールする

今回は、一般的によくログイン機能として使われる、

「メールアドレス」 + 「暗号化されたパスワード」

でログインできるようにするので、以下5つのパッケージをインストールしてください。

【ログイン機能パッケージ】

npm install --save passport
npm install --save passport-local

【暗号化パッケージ】

npm install --save bcrypt

【Session、Flashメッセージ用パッケージ】

npm install --save express-session
npm install --save connect-flash

準備する

では、開発しやすいようにテストユーザーを用意していきましょう。まず、以下のコマンドでSeedファイルを作成してください。

npx sequelize-cli seed:generate --name test-users

すると、「seeders/**************-test-users.js」というファイルが作成されるので、中身を以下のようにします。

'use strict';
const bcrypt = require('bcrypt');

module.exports = {
  up: (queryInterface, Sequelize) => {
    const now = new Date();
    return queryInterface.bulkInsert('Users', [
      {
        name: '太郎',
        email: 'taro@example.com',
        password: bcrypt.hashSync('secret', bcrypt.genSaltSync(8)),
        createdAt: new Date(),
        updatedAt: new Date()
      },
    ], {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Users', null, {});
  }
};

コードを保存したら以下のコマンドでマイグレーションを実行します。

npx sequelize db:migrate

Usersテーブルは、このようになります。

ログイン機能をつくる

では、実際にログイン機能を作っていきましょう。

ログイン機能の本体をつくる

まずは、「Passport.js」で実際にログインする部分を「auth.js」というファイルをルートフォルダにつくり、この中で実装します。(というのも、メインのファイルに全てコードを書くと長くなってしまい保守管理がしにくくなるためです😫)

const express = require('express');
const app = express();
const bcrypt = require('bcrypt');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('./models').User;

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'password'
  }, (email, password, done) =>  {

    User.findOne({
      where: {
        email: email
      }
    })
    .then(user => {

      if(user && bcrypt.compareSync(password, user.password)) {

        return done(null, user);  // ログイン成功

      }

      throw new Error();

    })
    .catch(error => { // エラー処理

      return done(null, false, { message: '認証情報と一致するレコードがありません。' });

    });

}));

// Session
passport.serializeUser((user, done) => {

  done(null, user);

});
passport.deserializeUser((user, done) => {

  done(null, user);

});

module.exports = passport;

やっていることは、以下のとおりです。

  1. データベースから「メールアドレス」でユーザーを取得
  2. もしユーザーが存在していたら、パスワードが一致するかチェック
  3. パスワードも正しければ、ログインする

ちなみに、この3つの項目でもし失敗があれば、「エラー処理」と書かれた部分が実行されることになります。

また「session」の部分は、ログイン後にユーザーデータを取得する部分になります。

auth.jsを読み込む

では、メインとなる「app.js」に戻って先ほどつくった「auth.js」を読み込んで、さらにミドルウェアを設定をしておきましょう。

const express = require('express');
const app = express();
const passport = require('./auth');
const session = require('express-session');
const flash = require('connect-flash');

// ミドルウェア
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(flash());
app.use(session({
  secret: 'YOUR-SECRET-STRING',
  resave: true,
  saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());

const authMiddleware = (req, res, next) => {
  if(req.isAuthenticated()) { // ログインしてるかチェック

    next();

  } else {

    res.redirect(302, '/login');

  }
};

はい!これでログインの本体の部分は完了です。
続いて、各ページに適用していきましょう。

ルートをつくる

今回作成するルートは、以下の3つです。

  • ログインフォーム ・・・ /login(GET)
  • ログイン実行 ・・・ /login(POST)
  • ログイン成功後のページ ・・・ /user(GET)※ログインしていないと強制的にリダイレクト

では、「app.js」を開いて以下のコードを追加してください。

// ログインフォーム
app.get('/login', (req, res) => {
  const errorMessage = req.flash('error').join('<br>');
  res.render('login/form', {
    errorMessage: errorMessage
  });
});

// ログイン実行
app.post('/login',
  passport.authenticate('local', {
    successRedirect: '/user',
    failureRedirect: '/login',
    failureFlash: true,
    badRequestMessage: '「メールアドレス」と「パスワード」は必須入力です。'
  })
);

// ログイン成功後のページ
app.get('/user', authMiddleware, (req, res) => {
  const user = req.user;
  res.send('ログイン完了!');
});

ここで重要なのが2点です。

1つ目は、「ログインフォーム」にあるreq.flash('error')です。ここでは、ログインに失敗したときのエラーメッセージをsessionから取得することになります。

そしてもうひとつが、「ログイン成功後のページ」のauthMiddlewareです。これは、先ほど設定したミドルウェアで、「/user」というページではログインをしていなければ強制的にリダイレクトされることになります。

ビューをつくる

では、先ほどのルートで設定したログインフォーム用のビューを作成しましょう。「views/login/form.mst」というファイルを作成し、中身を以下のようにします。

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ログイン・フォーム</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">ログイン・フォーム</div>
                <div class="card-body">
                    <form method="POST" action="/login">
                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">メールアドレス</label>
                            <div class="col-md-6">
                                <input id="email" type="text" class="form-control" name="email" autofocus>
                                {{ #errorMessage }}
                                <div class="alert alert-danger">
                                    {{ errorMessage }}
                                </div>
                                {{ /errorMessage }}
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">パスワード</label>
                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control" name="password">
                            </div>
                        </div>
                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">ログイン</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

ちなみに、mustacheテンプレート・エンジンを使うためには以下のような設定にしておいてください。

const mustacheExpress = require('mustache-express');
app.engine('mst', mustacheExpress());
app.set('view engine', 'mst');
app.set('views', __dirname + '/views');

テストしてみる

では、実際に「/login」にアクセスしてブラウザで確認してみましょう。

ログインフォームが表示されました。
まずは何も入力しないで送信してみます。

うまくエラー表示されました。
では次に、間違ったログイン情報を送信してみましょう。

こちらもうまくエラーが表示されました。
では最後に、正しいログイン情報を送信してみましょう。

はい!
リダイレクトされてログイン必須ページが表示されました。

成功です😊✨

ちなみに

例えば、「admin」「owner」「user」などユーザータイプでページの閲覧制限をするには、以下のようなミドルウェアを使うといいでしょう。

const adminAuthMiddleware = (req, res, next) => {
  if(req.isAuthenticated() && req.user.role === 'admin') {

    next();

  } else {

    res.redirect(302, '/login');

  }
};

※もちろん、データベース内にroleというフィールドを用意する必要があります。

ルートは、こうなります。

app.get('/admin', adminAuthMiddleware, (req, res) => {
  // 省略

});

おわりに

ということで、今回はExpressを使ったログイン機能を作ってみました。

今回はHTTP送信でログインを実装してみましたが、色々とsession関係でコードが複雑だったので、もしかするとAjaxを使ったログインの方がスムーズにいくかもしれません。

また、雑感としては、Laravelと比べると記述しないといけないコードが多いのでパッケージのinitialize()で必要最低限はすべて実行してくれたらうれしいなー、なんてナマケモノ・プログラマーの私は思ってしまいました(笑)

ログイン機能をすべて自前で用意することを考えると感謝感激!には間違いないないですけどね。

ではでは〜😊

この記事が役立ちましたらシェアお願いします😊✨