ニフクラmBaaSお役立ちブログ

スマホアプリ開発にニフクラmBaaS。アプリ開発に役立つ情報をおとどけ!

mBaaSを使ってブログを作る(その2)「認証機能を追加」

mBaaSをWebアプリケーションのバックエンドシステムとして使うデモです。前回はブログの基礎ができましたが、今回は認証機能を使って認証したユーザだけ記事を作成できるようにします。

セッション用ライブラリを追加

Expressでセッションを管理する場合は express-session というライブラリを使います。デフォルトではオンメモリのセッション管理なのでサーバを起動し直す度にデータが消えてしまいます。そこでデータを残すために nedb というMongoDBライクなデータベースライブラリも利用します。

npm install express-session connect-nedb-session nedb --save

インストールしたら app.js に設定を追加します。

app.use(session({
  secret: '何か適当な文字列',
  resave: false,
  saveUninitialized: true,
  store: new NedbStore({
    filename: path.join(__dirname, 'session.nedb')
  })
}));

認証判定を行う

まずトップページを表示した時に認証判定を行って、ログイン済みであれば新規投稿とログアウトボタン、まだの場合は新規登録とログインボタンを表示するようにします。

routes/index.js を修正します。

router.get('/', function(req, res, next) {
  var Post = ncmb.DataStore('Post');
  // 追加(ここから)
  if (req.session.currentUser) {
    ncmb.sessionToken = req.session.currentUser.sessionToken;
  }
  // 追加(ここまで)
  Post.order('createDate', true).fetchAll()
    .then(function(posts) {
      // 変更
      res.render('index', { posts: posts, currentUser: req.session.currentUser});
    })
});

セッションデータは req.session.currentUser にて取得できます。デフォルトは何も入っていませんので undefined になります。これをビュー表示の際にも利用します。

views/index.js を修正します。新規投稿ボタンのところを次のように変更します。

<div class="col-md-6">&nbsp; <!-- 変更 -->
 </div>
<div class="col-md-2">
  <a href="/posts/new" class="btn btn-info">新規投稿</a>
<div class="col-md-6">
  <% if (typeof currentUser == 'undefined') { %>
    <a href="/login" class="btn btn-warning">ログイン</a>
    <a href="/register" class="btn btn-info">ユーザ登録</a>
  <% } else { %>
    <%= currentUser.userName %> さん
    <a href="/posts/new" class="btn btn-info">新規投稿</a>
    <a href="/logout" id="logout" class="btn btn-warning">ログアウト</a>
  <% } %>
</div>

ログインしているかどうか(currentUserがあるかどうか)を判断して表示分けを行っています。

新規登録ページを作る

/register にアクセスした時の処理を作ります。これは routes/index.js に作ります。

router.get('/register', function(req, res, next) {
  res.render('users/new');
});

これを表示するためのビューを views/users/new.ejs として作ります。

<!DOCTYPE html>
<html>
  <head>
    <title>ユーザ登録</title>
    <link rel='stylesheet' href='/vendors/bootstrap/dist/css/bootstrap.min.css' />
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-md-8 col-md-offset-2">
          <h1>ユーザ登録</h1>
          <p>IDとパスワードを入力してください</p>
          <form id="register_form">
            <div class="form-group">
              <label for="inputUserName">ユーザID</label>
              <input type="text" class="form-control" id="inputUserName" name="userName" placeholder="ユーザID" />
            </div>
            <div class="form-group">
              <label for="inputPassword">パスワード</label>
              <input type="password" id="password" class="form-control" name="password" />
            </div>
            <button type="submit" class="btn btn-default">登録</button>
          </form>
        </div>
      </div>
    </div>
    <script src='/vendors/jquery/dist/jquery.min.js'></script>
    <script src='/vendors/bootstrap/dist/js/bootstrap.min.js'></script>
    <script src='/javascripts/app.js'></script>
  </body>
</html>

ここではブログの投稿と同じく、ログインをAjaxで処理します。 public/javascripts/app.js を次のように変更します。同じような処理が多いので、 sendForm としてまとめてあります。 sendForm では指定されたURLに対してAjax処理を行い(デフォルトはPOST)、指定がなければトップにリダイレクトします。指定があればコールバックします。

$('#post_form').on('submit', function(e) {
  e.preventDefault();
  sendForm({url: '/posts', form: $(this).serialize()})
});

// 追加
$("#register_form").on('click', function(e) {
  e.preventDefault();
  sendForm({url: '/users', form: $(this).serialize()}, function(data) {
    location.href = "/login"
  });
});

// Ajax処理をして戻す
function sendForm(params, callback) {
  params.method = params.method || 'POST'
  return $.ajax({
    url: params.url,
    type: params.method,
    data: params.form
  })
  .then(function(data) {
    if (typeof callback == 'function') {
      callback(data);
    } else {
      location.href = "/";
    }
  },
  function(err) {
    console.error(err);
  });
}

最後にAjaxの処理を行う POST /users を作成します。これは routes/users.js に実装します。ユーザオブジェクト(user)のユーザ名(userName)とパスワード(password)にデータを適用して signUpByAccount を実行するとユーザ登録が完了します。

router.post('/', function(req, res, next) {
  var acl = new ncmb.Acl();
  acl.setPublicReadAccess(true);
  
  var user = new ncmb.User();
  user
    .set("userName", req.body.userName)
    .set("password", req.body.password)
    .set("acl", acl)
    .signUpByAccount()
      .then(function(user) {
        res.status(201).json(user);
      })
      .catch(function(err) {
        res.status(401).json(err);
      })
});

ユーザデータについては setPublicReadAccess を有効にしています。これはユーザ一覧を取得する場合を想定しています。デフォルトは読み込み不可なので注意してください。

ログイン処理を追加する

ユーザ登録ができましたので、次にログイン処理を実装します。これはまず /login にアクセスした時のページを作成します。ルーティングを処理するのは routes/index.js になります。

router.get('/login', function(req, res, next) {
  res.render('sessions/new');
});

このページを表示する views/sessions/new.ejs を作成します。

<!DOCTYPE html>
<html>
  <head>
    <title>ログイン</title>
    <link rel='stylesheet' href='/vendors/bootstrap/dist/css/bootstrap.min.css' />
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-md-8 col-md-offset-2">
          <h1>ログイン</h1>
          <p>IDとパスワードを入力してください</p>
          <form id="login_form">
            <div class="form-group">
              <label for="inputUserName">ユーザID</label>
              <input type="text" class="form-control" id="inputUserName" name="userName" placeholder="ユーザID" />
            </div>
            <div class="form-group">
              <label for="inputPassword">パスワード</label>
              <input type="password" id="password" class="form-control" name="password" />
            </div>
            <button type="submit" class="btn btn-default">ログイン</button>
          </form>
        </div>
      </div>
    </div>
    <script src='/vendors/jquery/dist/jquery.min.js'></script>
    <script src='/vendors/bootstrap/dist/js/bootstrap.min.js'></script>
    <script src='/javascripts/app.js'></script>
  </body>
</html>

ページ構成はユーザ登録と同じです。ただしAjax処理用としてフォームのIDをlogin_formにしてあります。

public/javascripts/app.js に処理を追加します。

$("#login_form").on('submit', function(e) {
  e.preventDefault();
  sendForm({url: '/sessions', form: $(this).serialize()});
});

これで /sessions に対してPOSTメソッドで処理を実行し、問題がなければトップページにリダイレクトするようになります。

最後に POST /sessions を処理するために routes/sessions.js を書きます。ユーザ登録と同じように ncmb.User を作成しますが、使うのはloginメソッドになります。そしてログイン処理が成功したら、 req.session.currentUser に対してログインしたユーザ情報を適用します。これでログイン判定処理が使えるようになりました。

var express = require('express');
var router = express.Router();
var ncmb   = require('../libs/ncmb');

router.post('/', function(req, res, next) {
  var User = new ncmb.User({userName: req.body.userName, password: req.body.password});
  User.login()
    .then(function(user) {
      req.session.currentUser = user;
      res.status(200).json(user)
    })
    .catch(function(err) {
      res.status(400).json(err)
    });
});

module.exports = router;

追加で app.js へsessions.jsを追加するのを忘れないでください。

var sessions = require('./routes/sessions');
  :
app.use('/sessions', sessions);

ログアウト処理を作る

ログアウトは #logout をクリックしたタイミングで処理します。/logout へのアクセスになりますが、GETではなくDELETEメソッドでアクセスします。ログアウト=セッションを削除するという意味においてRESTful APIの原則に沿っていること、GETアクセスでは悪意を持った人たちによって簡単にログアウトさせられてしまう問題があります。そこでAjax側で次のように処理を行います。

$("#logout").on('click', function(e) {
  e.preventDefault();
  sendForm({url: '/sessions', method: "DELETE"});
});

DELETEメソッドで /sessions に対してアクセスします。処理が完了したらトップページにリダイレクトします。

この処理を routes/sessions.js に対して記述します。ログアウト処理はセッションデータの削除だけとしています。 ncmb.currentUser.logout メソッドを使うこともできます。

router.delete('/', function(req, res, next) {
  delete req.session['currentUser'];
  res.status(200).json({})
});

新規投稿画面を変更する

さてせっかく認証処理を作ったので投稿処理に使ってみたいと思います。変更点として以下が挙げられます。

  • 公開、非公開設定ができるように
  • 編集権限を設定する

公開、非公開設定ができるように

公開、非公開は新規投稿画面で指定します。 views/posts/new.ejs を次のように修正します。

<div class="container">
  <div class="row">
    <div class="col-md-8 col-md-offset-2">
        :
      <form id="post_form">
        <!-- 追加する -->
        <div class="checkbox">
          <label>
            <input type="checkbox" name="public" value="true"> 公開する
          </label>
        </div>
        <!-- 追加ここまで -->
        <button type="submit" class="btn btn-default">投稿</button>
      </form>
    </div>
  </div>
</div>

このpublicという項目があるかどうかで公開/非公開を切り替えます。

この処理を routes/posts.js に記述します。

router.post('/', function(req, res, next) {
  
  // 追加。現在ログインしているユーザを作成
  var currentUser = new ncmb.User(req.session.currentUser);
  
  let Post = ncmb.DataStore('Post');
  var post = new Post;
  post.set('title', req.body.title);
  post.set('body', req.body.body);
  post.set('public', (req.body.public == 'true'));
  
  // ACLを設定
  var acl = new ncmb.Acl;
  if (req.body.public == 'true') {
    // 公開する設定だった場合は読み込み権限を設定する
    acl.setPublicReadAccess(true);
  } else {
    // 非公開の場合は現在のユーザだけに読み込み権限を設定する
    acl.setUserReadAccess(currentUser, true)
  }
  // 書き込み権限は現在のユーザのみ
  acl.setUserWriteAccess(currentUser, true)
  // ACLを設定
  post.set('acl', acl);
  post.save()
    .then(function(obj) {
      res.status(201).json(obj);
    })
    .catch(function(err) {
      res.status(401).json(err);
    })
});

これで完了です。

記事一覧取得処理を変更する

最後に記事一覧を取得する処理を変更します。ログイン時のセッションを追加することで自分の投稿であれば非公開記事でも取得できるようになります。セッションデータを適用するには次のように行います。ログインしている場合はセッショントークンを取り、ログインしていない場合はnullを適用します。

ncmb.sessionToken = req.session.currentUser ? req.session.currentUser.sessionToken : null;

ここまでの処理で、新規登録とログイン、権限を付与した投稿ができるようになりました。非公開記事はログイン状態だと見られて、未ログインでは閲覧不可となります。

ユーザ登録した人だけ見せたい、といった場合はユーザ登録時に特定のロールに加え、そのロールに対してだけ閲覧権限を付与すればOKです。

面白いのはセッショントークンだけ適用すればユーザ権限に応じたデータが取得できるということです。検索条件を変更したりする必要はありません。そういった権限周りのプログラミングミスによるデータが公開されてしまうという問題が発生しないのが利点です。

今回のコードはNCMBMania/ncmb_blog_nodejsにアップロードしてあります。不明点があれば参考にしてください。