Node.js + express で express-session でセッション管理する方法

Node.js の express コマンドで、アプリケーションを自動生成した後、ログイン／ログアウトを管理する機能について学んだ時のメモです。　O'REILLY Nodeクックブックを参考に実装しようとしたら、書籍が書かれた時期と現在利用しているバージョンが異なるため、とっても苦心したので、忘れない様にメモです。

## express コマンドでのアプリの生成
テンプレートエンジンは、デフォルトのJADEを利用し、CSSプロプロセッサは、stylus を利用してアプリの雛形を作成します。

~~~
$ express --css stylus myapp2

   create : myapp2
   create : myapp2/package.json
   create : myapp2/app.js
   create : myapp2/public
   create : myapp2/public/javascripts
   create : myapp2/public/images
   create : myapp2/public/stylesheets
   create : myapp2/public/stylesheets/style.styl
   create : myapp2/routes
   create : myapp2/routes/index.js
   create : myapp2/routes/users.js
   create : myapp2/views
   create : myapp2/views/index.jade
   create : myapp2/views/layout.jade
   create : myapp2/views/error.jade
   create : myapp2/bin
   create : myapp2/bin/www

   install dependencies:
     $ cd myapp2 && npm install

   run the app:
     $ DEBUG=myapp2:* npm start

~~~

## フォント色とバックグランド色の変更
本題からそれるのですが、CSSプリプロセッサのソースを変更して、フォント色とバックグランド色を変更します。

~~~file:myapp2/public/stylesheets/style.styl
body
  padding: 50px
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
  color: white                    <<-- 追加
  background-color: #26343F       <<-- 追加
a
  color: #00B7FF
~~~

## セッション管理のミドルウェアの作成
このExpress で開発したウェブ・サイト全体のセッションを管理して、ログインしない者に中身を見せない様にするためのミドルウェアを作ります。 そして、app.js に設定して、アクセスの際にログイン管理のミドルウェアを通過する様にします。
まずは、ミドルウェア部分の実装です。

~~~file:myapp2/login.js
var users = { 'tkr': 'password' };  // ユーザーパスワード                                                            

module.exports = function(req, res, next) {

    var method = req.method.toLowerCase();
    var user = req.body;
    var logout = (method === 'post' && req.url === '/logout');
    var login = (method === 'post' && user);

    // ログアウト                                                                                                    
    if (logout) {
        delete req.session.user;
    }

    // ログイン                                                                                                      
    if (login) {
        Object.keys(users).forEach(function(name) {
            if (user.name === name && user.pwd === users[name]) {
		req.session.user = {
                    name: user.name,
                    pwd: user.pwd
		};
            }
	});
    }

    // セッションが無ければ ログイン画面へ                                                                           
    if (!req.session.user) {req.url = '/'}

    next();
}
~~~

参考にした O'REILLY Nodeクックブック p200 のサンプルコードから、結構多くの変更があります。 HTTP DELETE はブラウザによって、動作しなかったので、HTTP POST を利用する様に変更しました。 それから、廃止された機能を削ってあります。 ユーザーとパスワードは、このコードにベタ書きしていますが、実際はデータベースに保管する必要があります。また、パスワードもクリアテキストで保管するのではなく、ハッシュにして保存し、照合はハッシュで実施する事で、パスワードを知られない様にする必要があります。 ここでは本題では無いので、簡単にコードに留めます。
ユーザー認証が成功すると req.session.user に保管します。

次に、Expressアプリケーションの本体である app.js に変更を加えます。

~~~file:myapp2/app.js変更部分
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');  // ここもexpress の一部から、独立に変更されている
var session = require('express-session'); // <<-- 追加  後に対応するnpm install を実行する                                                              

var routes = require('./routes/index');
var users = require('./routes/users');
~~~

参考にした O'REILLY Nodeクックブック p196では、express コマンドに --session オプションを付与して自動的にモジュールを追加との解説がありますが、node.js v4.x系、v6.x系の express コマンドからは、このオプションが廃止されています。 それから、express.session()がexpressの一部から、別のモジュールに独立した様ですから、この辺の実装が変わります。

次のコードも、同じくapp.jsの変更部分ですが、先ほど追加した login.js をミドルウェアとして設定する部分になります。

~~~file:myapp2/app.js変更部分
app.use(require('stylus').middleware(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));

// 　ここから追加                                                                             
key = {
    secret: 'zakuzaku',
    resave: false,
    saveUninitialized: true,
    cookie: {
	maxAge: 180000
    }
};
app.use(session(key));
// 
app.use(require('./login'));
app.use('/', routes);
app.use('/logout', routes);
app.use('/users', users);
//  ここまで

// catch 404 and forward to error handler                                                                            
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// 任意のページもログイン対象にする   追加
app.use('/:page', routes);

// error handlers     
~~~

app.use(session(key))は、セッションを追跡するための設定で、この設定ではオンメモリとなっていますので、スケール・アウトする場合には、KVSに保存する必要があります。

- app.use(require('./login')) のルーティング部分は、loginモジュールを通過させる事で、ログインしていないユーザーにコンテンツを参照できなくします。
- app.use('/', routes) は、express がデフォルトで組み込むルーティングですが、routes.js と index.jade を変更する事で、/ をアクセスする場合、未ログインの場合は、ログインをページを表示し、ログイン済みの場合は、挨拶文を表示します。
- app.use('/logout', routes)は、ログアウト処理のルーティングです。 このURLをアクセスするとログイン情報を削除する事でログアウトします。
- app.use('/users', users)は、express がデフォルトで設定したルーティングです。ログインしなければ、/usersをアクセスできなくなります。
- // catch 404 から始るコードを、ここに挟む事で、ログイン後に、該当ページが存在しない場合、404 Not Found を表示します。 また、未ログインの場合は、ログイン・ページを表示して、内部構造を推定させません。
- app.use('/:page', routes) は、/ 以下の任意のページアクセスも、routes.js モジュールへルーティンします。

## URLルートのコード変更
app.js から URLルーティングによって呼び出されるコード index.js です。 app.js の中で、app.use で定義する事で、HTTP メソッドに関わらず該当のURLルートのアクセスが、このコードへ飛んできます。 このため、post と get の処理をそれぞれ書いておく必要があります。 app.js の中で、app.get や app.post でモジュール内の特定のメソッドに飛ばすこともできますが、app.use を利用することで、app.js をシンプルに保つ事が可能になります。

~~~file:myapp2/routes/index.js
var express = require('express');
var router = express.Router();

function render(req, res, next) {
    res.render('index', { title: 'Express', user: req.session.user });
}

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

router.post('/', function(req, res, next) {
    render(req, res, next);
});

module.exports = router;
~~~

## JADE テンプレートの変更と追加
expressコマンドで生成される index.jade の最終行に include login.jade を追加します

~~~file:myapp2/views/index.jade
extends layout

block content
  h1= title
  p Welcome to #{title}

  include login.jade
~~~
ユーザーがログインしている際は「こんにちは」表示して、未ログイン状態では、ログインのフォームを表示します。

~~~file:myapp2/views/login.jade
if user
   form(method='post', action='/logout')
     input(name='logout', type='hidden', value='DELETE')
     p #{user.name}さん、こんにちは!
       a(href='javascript:', onClick='forms[0].submit()') [ログアウト]
else
   p ログインしてください
   form(method='post', action='/')
     fieldset
       legend ログイン
       p
         label(for='name') ユーザー名：
         input(name='name')
       p
         label(for='pwd') パスワード：
         input(type='password', name='pwd')
       input(type='submit')
~~~

## モジュールのインストールとアプリ起動
express-session が独立のモジュールになっているため、追加でインストールします。また、package.jsonに書き込みます。 package.json に登録された残りのモジュールをすべてインストールするために、npm install を実行します。

~~~
myapp2$ npm install --save express-session
myapp2$ npm install
~~~
次に、アプリを起動します。

~~~
myapp2$ npm start

> myapp2@0.0.0 start /home/tkr/express_work2/myapp2
> node ./bin/www

GET /favicon.ico 200 1145.927 ms - 470
GET / 200 157.050 ms - 470
GET /stylesheets/style.css 200 372.358 ms - 154
~~~

## 実行結果

ログイン画面
![スクリーンショット 2017-05-13 22.38.02.png](https://qiita-image-store.s3.amazonaws.com/0/42829/2cbb3094-44fc-eebb-ded7-2ff56f22e635.png)

ログイン成功時の画面
![スクリーンショット 2017-05-13 22.38.39.png](https://qiita-image-store.s3.amazonaws.com/0/42829/7f0aaba9-edcc-8d58-7af6-c16a6b4306f3.png)

/users をアクセスした場合の画面、ログインしていない場合は、ログイン画面が表示されます。
![スクリーンショット 2017-05-13 22.38.57.png](https://qiita-image-store.s3.amazonaws.com/0/42829/916e2ba7-5b09-83f5-1649-ff38e1a40a41.png)

ログイン後に、存在しないURLを指定した場合のデバック画面
![スクリーンショット 2017-05-13 22.40.42.png](https://qiita-image-store.s3.amazonaws.com/0/42829/7329ddd8-8232-5534-8695-b9ae9f47a404.png)

ブラウザに保存されたセッション情報
![スクリーンショット 2017-05-13 22.46.43.png](https://qiita-image-store.s3.amazonaws.com/0/42829/b219dd29-b0cc-0df0-8cd0-1ea7eecf1a4f.png)

以上