Node.js + Express.js + express-sessionでセッションにデータ格納する方法


[express-session](https://github.com/expressjs/session)がバージョンアップしているせいか、ネット上のサンプル見て実装してたらハマったのでメモ。

作るもの
===

* express-generatorで作成した雛形をベースにする
* ユーザ認証はしない（別の機会に）
* セッションストアはひとまずメモリストア（デフォルト、再起動で初期化）
* アプリケーションルートにアクセスした時に、ユーザ名がセッションに無かったらログイン画面（というかユーザ名入力画面）にリダイレクトさせる
* ユーザ名が入力されたらセッションに格納し、アプリケーションルートにリダイレクトさせる

SPAなWEBアプリケーションにユーザ入力を挟むようなルーティングのサンプルです。

出来上がったもの
===

githubにあげておきました。
[このコミットでの差分](https://github.com/moomoo-ya/express-session-sample/commit/fab66a9072ec4e196d622b4036dbb196368b75f0)を見れば作業内容は把握できるかと思います。

https://github.com/moomoo-ya/express-session-sample

作業の手順
===

`express`コマンドで雛形作成
---

```bash
$ express [アプリケーション名]
```

サンプルで`--hbs`とか`--ejs`とかしていないので、そういう方は適宜読み替えてください。
あまり重要な部分ではないです。

app.jsを修正
---

app.jsの修正が一番手間な部分です。

### モジュールの読み込み

```javascript:app.js
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session'); // 追加
```

`express-session`を読み込みます。

### ルーティング定義ファイルを読み込み

```javascript:app.js
var routes = require('./routes/index');
var users = require('./routes/users');
var login = require('./routes/login');  // 追加
```

ルーティングを実装する予定の`routes/login.js`を読み込みます。

### express-sessionモジュールを設定

```javascript:app.js
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 以下追加 
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 30 * 60 * 1000
  }
}));
```
session(=express-session)を利用する設定をします。

`secret`はCookieの暗号化に利用するキーなので適宜変更して利用します。アプリケーションリリース後に変更したい場合は、
`secret: ['(新しいキー)', '(今までのキー)', '(今までのキー)']`
とすることで先頭のキーを暗号化に用いて、復号化には配列中のいずれかのキーを利用するらしいです（未検証）。

`resave`はtrueだとセッションストアにアクセスするたび（＝セッションチェックする領域にリクエストするたび）にセッションを作り直すらしいので、基本はfalseでOKかと思います。

`saveUninitialized`はtrueだと未初期化状態のセッションも保存されるようになるようですが、一般的に使われるログインセッションの処理では未初期化のセッションは不要なので、falseでOKかと思います。

resaveとsaveUninitializedの挙動は今ひとつ自信がない……。

`cookie.maxAge`はCookieの有効期限をミリ秒で設定します。指定なし、もしくはnullだとブラウザデフォルトの挙動（一般的にはブラウザを閉じたらCookie削除）になります。

### セッションチェック処理を実装

この部分はmiddlewareとして別ファイルに分割した方がスマートかもですね。

```javascript:app.js
var sessionCheck = function(req, res, next) {
  if (req.session.user) {
    next();
  } else {
    res.redirect('/login');
  }
};
```

セッション情報(`req.session.*`、今回は`req.session.user`に格納されている)の有無をチェックして後処理に継続`next()`するか、ログイン画面にリダイレクトするか分岐させています。

### ルーティングの追加とセッションチェック処理の追加

```javascript:app.js
app.use('/login', login);  // 追加
app.use('/', sessionCheck, routes);  // sessionCheckを前処理に追加
app.use('/users', users);
```

`app.use([path], function[, function...])`のpathの部分は指定したパス以下に適用してしまうため、`app.use('/login', login);`は`app.use('/', sessionCheck, routes);`よりも前に追加しないと、sessionCheckが適用されてしまい永遠にログイン画面にリダイレクトが発生してエラーになります。



ログイン処理のルーティングを定義
---

`app.use('/login', login);`から呼ばれているので'/'が'/login'を表すことになります。

```javascript:routes/login.js
var express = require('express');
var router = express.Router();

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

router.post('/', function(req, res, next) {
  if(req.body.userName) {
    req.session.user = {name: req.body.userName};
    res.redirect('../');
  } else {
    var err = '入力が正しくありません。確認して再入力してください。';
    res.render('login', {error: err});
  }
});

module.exports = router;
```

`router.get('/', ...)`の部分はログイン画面('/login')へのアクセスなので単純にlogin.jadeを表示するだけです。

`router.post('/', ...)`の部分はログイン画面のFORMからPOSTされてアクセスされます。
ここでは`req.body.userName`の有無のみで分岐していますが、パスワード認証の場合は`req.body.password`なども参照してDB参照した上で分岐になります。

`req.session`以下にデータを格納することでセッションへのデータ格納が実現できます。

入力データが正しくない場合はエラーメッセージを用意してlogin.jadeに渡します。

ログイン画面の実装
---

エラー表示と/loginへのPOSTのみなので参考までに。

```jade:views/login.jade
extends layout

block content
  h1 Login
  p.caution= error
  form(method='POST',action='/login')
    input(type='text',name='userName',placeholder='username')
    input(type='submit',value='Login')
    
```

パスワード認証の場合は
`input(type='password',name='password')`
などを適宜追加してください。

---

セッションストアはメモリストア以外を使ったほうが実用的と思うので、大規模システムならredisなど、スケール考えつつ手軽に始めるならnedb（mongo互換のファイルDB）などにしておいたほうが良いと思います。

「express-sessionモジュールを設定」の部分で設定追加することで出来るので、気が向いたらまた記事にします。