チュートリアル
ECHOPFをバックグラウンドにした会員制サイトを作ろう(その2:会員ログイン/ログアウト)
コンテンツへのアクセス制限をするためにログイン認証させる仕組みはよく使われます。しかしデータベースを用意したり、セキュアに認証システムを提供するには様々な用意が必要です。そんな手間を減らすべく、ECHOPFを認証システムとして使ってみましょう。
作るもの
このチュートリアルではJavaScript SDKを使って、Node.jsでExpressサーバでシステムを作ります。そして3回に分けて会員登録、ログインとログアウト、そして会員情報の編集という機能を実装します。前回は会員登録の実装方法を紹介しました。二回目となる今回はログイン/ログアウト処理について解説します。
必要なもの
Node.js
Node.jsはサーバサイド、ローカルコンピュータ上で動作するJavaScriptエンジンになります。Node.jsを使うことでJavaScriptを使ってWebアプリケーションであったり、ローカルコンピュータ上で動作するソフトウェアが開発できるようになります。
ダウンロードはNode.jsの公式プロジェクトサイトから行えます。インストーラーを実行するだけで完了します。
セッション管理について
ログイン処理を行うと、その認証情報が有効であるという情報をセッションに持たせます。もしユーザIDなどを直接Webブラウザに渡してしまうと、悪意を持ったユーザがユーザIDを書き換えると別なユーザになりすませてしまいます。そういった問題を防ぐためにWebブラウザにはセッションID(ユニークで推測できない文字列)だけを渡して、そのセッションIDと実際のデータ(ユーザ名など)の紐付けはデータベースで管理するのが一般的です。
しかしこの方法の問題点としてはデータベースを用意しなければならないことと、複数デバイスからのログインをサポートしようと思うとさらに管理が煩雑化することでしょう。そこで最近注目されているのがJSON Web Tokenという仕組みです。
JSON Web Tokenは改変できないトークン文字列になります。全部で3つの構成要素となっており、それぞれがドット(.)でつながっています。
- ヘッダー
- ペイロード
- 署名
ペイロード部分はユーザ名であったり、アクセストークンなど自由な文字をJSON形式にし、Base64でエンコードします。同様にヘッダーや署名についてもBase64でエンコードしてあります。利用する際にはペイロード部分をデコードし、JSONとしてパースすればOKです。このデータは署名で検証可能なので、もし改ざんされていれば検知できるようになっています。セッションID方式と異なり、データベース不要で使えること、URLセーフ(URLとして使える文字列のみ)であるというのが利点です。ただし文字列としてはかなり長くなります。
例えば以下のような文字列になります。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NUb2tlbiI6Ijlyc2t2aHZhMzZlbWhxdm5rc2NodjA3bDA3IiwicmVmSWQiOiI3OTU0IiwibG9naW5faWQiOiJ0ZXN0VXNlciIsIm5hbWUiOiJBdHN1c2hpIE5ha2F0c3VnYXdhIiwiZW1haWwiOiJ0ZXN0QG1vb25naWZ0LmpwIiwiaWF0IjoxNTIyMzczNDkxLCJleHAiOjE1MjIzNzcwOTF9.05dmzKN5IH76T6-wt78tPYtL3y9TfAbPwP3svWo-9y4
これをドットごとに分割します。
- ヘッダー
 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- ペイロード
 eyJhY2Nlc3NUb2tlbiI6Ijlyc2t2aHZhMzZlbWhxdm5rc2NodjA3bDA3IiwicmVmSWQiOiI3OTU0IiwibG9naW5faWQiOiJ0ZXN0VXNlciIsIm5hbWUiOiJBdHN1c2hpIE5ha2F0c3VnYXdhIiwiZW1haWwiOiJ0ZXN0QG1vb25naWZ0LmpwIiwiaWF0IjoxNTIyMzczNDkxLCJleHAiOjE1MjIzNzcwOTF9
- 署名
 05dmzKN5IH76T6-wt78tPYtL3y9TfAbPwP3svWo-9y4
この時、ペイロードをBase64デコードすると次のようになります。これはプログラムから簡単に閲覧できる情報です。
{
    "accessToken": "9rskvhva36emhqvnkschv07l07", 
    "email": "test@moongift.jp", 
    "exp": 1522377091, 
    "iat": 1522373491, 
    "login_id": "testUser", 
    "name": "Atsushi Nakatsugawa", 
    "refId": "7954"
}
ただしこの情報を改変してサーバに送信すると、署名が不一致になりますのでエラーとなります。これがJSON Web Tokenの利点です。
今回はこのJSON Web Tokenを使って認証データを管理したいと思います。
ログイン処理について
認証処理は routes/sessions.js に実装します。まずファイルを作成後、 app.js にて読み込みます。
var sessions = require('./routes/sessions');
app.use('/sessions', sessions);
sessions.js の内容は最初、次のようになります。routes/users.js の内容とほぼ変わりません。
const express = require('express');
const router = express.Router();
const ECHOPF = require('../ECHO.min');
const config = require('../config');
ECHOPF.initialize(
    config.accountDomain,
    config.applicationId,
    config.applicationKey
);
/* GET login page */
router.get('/new', (req, res, next) => {
  res.render('sessions/new');
});
module.exports = router;
ログインページを作る
GET /sessions/new にアクセスした時のログイン入力画面を作ります。これはユーザ登録時のテンプレートから不要な情報を削っただけです。
extends ../layout
block content
  div.row.justify-content-md-center
    div.col-md-auto
      h2 ログインしてください
      if error
        div.alert.alert-warning
          span= error.message
          if error.details
            ul
              each val, key in error.details
                li #{key} : #{val.message}
      form(action="/sessions",method="post")
        div.form-group
          label(for=inputLoginId) ログインID
          input.form-control(type="text",name="login_id",placeholder="ログインID")
        div.form-group
          label(for=inputPassword) パスワード
          input.form-control(type="password",name="password")
        button.btn.btn-primary(type="submit") ログイン

ログイン処理を作る
ログイン処理は上記テンプレートでも書かれている通り、 POST /sessions にて行います。
router.post('/', (req, res, next) => {
});
ECHOPFでのログイン
ECHOPFにおけるログイン処理は次のように実装します。 member というのは前回指定した会員管理用のインスタンスIDになります。
new ECHOPF.Members.login(
  'member',
  req.body.login_id,
  req.body.password)
これは Promise を返しますので、ログインがうまくいけば then で結果を受け取れます。エラーの場合は二つ目の引数です。この場合には再度ログイン画面を表示します。
new ECHOPF.Members.login(
  'member',
  req.body.login_id,
  req.body.password)
  .then(user => {
    // ログインがうまくいった場合
  }, err => {
    // ログインに失敗した場合
    res.render('sessions/new', {
        error: err
    });
  });
JSON Web Tokenの準備
Node.js でJSON Web Tokenを使うのは jsonwebtoken というライブラリになります。まずこのライブラリをインストールします。
$ npm install jsonwebtoken --save
インストールが終わったら routes/sessions.js にて読み込みます。
const jwt = require('jsonwebtoken');
設定ファイル(config.json)に、secretというキーで適当な文字列を設定しておきます。これはJSON Web Tokenの署名や検証に使いますので漏洩しないよう注意してください。
{
    "accountDomain": "YOUR.echopf.com",
    "applicationId": "YOUR_APPLICATION_ID",
    "applicationKey": "YOUR_APPLICATION_KEY",
    "secret": "YOUR_SECRET"
}
ログイン後の処理でJSON Web Tokenを生成
JSON Web Tokenのライブラリをインストールしたら、ログイン処理成功時にJSON Web Tokenを生成します。アクセストークンやログインID、名前などをJSON Web Tokenにします。
const json = {
  accessToken: user.get('access_token'),
  refId: user.refid,
  login_id: user.get('login_id'),
  name: user.get('contents').name,
  email: user.get('contents').email
}
var token = jwt.sign(json, config.secret, {
  expiresIn: '1h'
});
今回は有効期限を1時間としています。この長さはECHOPFの認証時間に合わせておくのが良いでしょう。
そしてJSON Web TokenはURLセーフなので、URLにそのままクエリとして渡しても良いですが、URLが分かりづらく感じるのでCookieに格納することにします。
res.cookie('token', token);
最後、ログイン完了後にトップページにリダイレクトします。
return res.redirect('/');
これで認証処理が完成です。全体の流れは次のようになります。
router.post('/', (req, res, next) => {
  new ECHOPF.Members.login(
    'member',
    req.body.login_id,
    req.body.password)
    .then(user => {
      const json = {
        accessToken: user.get('access_token'),
        refId: user.refid,
        login_id: user.get('login_id'),
        name: user.get('contents').name,
        email: user.get('contents').email
      }
      var token = jwt.sign(json, config.secret, {
        expiresIn: '1h'
      });
      res.cookie('token', token);
      return res.redirect('/');
    }, err => {
      res.render('sessions/new', {
          error: err
      });
    });
});

ログイン判定
ログイン判定ですが、これはCookieの文字列を使えば簡単です。認証必須ページでは異なる実装方法になりますが、必須ではないページでは次のようにCookieの文字列を分解してチェックできます。
router.get('/', (req, res, next) {
  const token = req.cookies.token;
  const json = token
    ? JSON.parse(new Buffer(token.split('.')[1], 'base64').toString('ascii'))
    : {};
  res.render('index', { title: 'Express', json: json });
});
これで認証している場合にはユーザ名が出せるようになりました。
なお、Cookieの文字列はサーバサイドだけでなく、Webブラウザでも document.cookie 取得できます。これによってサーバサイドを使わずにログイン済みかどうかの判定ができます。ただし改ざんされている可能性がありますので、あくまでもユーザ名の表示などに使うべきで、セキュアなデータにアクセスしない方が良いでしょう。
ログアウト処理を作る
JSON Web Tokenでのログアウト処理は簡単で、今回の場合はCookieからトークンを削除してしまうだけです。データベースを使っていないので実装がとても簡単です。今回は DELETE /sessions でログアウト処理を実装します。
DELETEメソッドのサポート
Expressでは標準ではDELETEメソッドをサポートしていません。そのため method-override というライブラリを使います。
$ npm install method-override --save
このライブラリを使って擬似的にDELETE(またはPUT)メソッドをサポートします。これは app.js に実装します。 _method という情報が送られてきたら、そこに入っている文字列にHTTPメソッドを変更します。
var methodOverride = require('method-override');
  // 省略:
app.use(methodOverride((req, res) => {
  if (req.body && typeof req.body === 'object' && '_method' in req.body) {
    const method = req.body._method;
    delete req.body._method;
    return method;
  }
}));
HTMLフォーム
HTMLは views/index.jade に実装します。
form(action="/sessions",method="post")
  input(type="hidden",name="_method",value="delete")
  button ログアウト
ログアウト処理の実装
ログアウト処理を DELETE sessions て実装します。Cookieを消すのは res.clearCookie です。
router.delete('/', (req, res, next) => {
  res.clearCookie('token');
  return res.redirect('/');
});
まとめ
ここまでの実装でログイン/ログアウト処理の実装が完了しました。ECHOPFを使うことで、バックエンドで認証が実現できます。さらにJSON Web Tokenを使うことで、データベースを用意せずにセキュアにセッション管理が実現できます。アプリケーションサーバだけであればスケーリングも容易です。
ここまでの実装はechopfcom/Echopf_Member at v2にアップロードしてあります。実装時の参考にしてください。次回はログインした人だけに提供される機能として、会員情報編集機能を実装します。
