Edited at

Express+Passportで簡単に認証機能を実現

More than 1 year has passed since last update.


導入

現在ウェブアプリケーションの勉強をしています。

その過程で、Node.jsとExpress構成でサイトを作成しているのですが、認証機能を追加したいと思いました。

認証にはPassportというライブラリが便利だと、知人から聞きましたので、今回実際に実装してみて、

使い方などをまとめました。


1. 環境構築

まずはベースとなる環境の構築から始めます。

Node.jsやnpmはインストール済みの想定で進みます。

// expressコマンドを使用するためにexpress-generatorをグローバルインストール

$ npm install express-generator -g

// グルーバルインストール先を確認したい場合は以下のコマンドを実行
$ npm root -g

// express環境を構築
$ express passportTest
$ cd passportTest
$ npm install

// 必要なライブラリをインストール
$ npm install passport --save
$ npm install passport-local --save
$ npm install express-session --save

// サーバを実行
$ node ./bin/www

動作確認を行いましょう。

ブラウザで「localhost:3000」にアクセスしてください。

「welcome to express」が表示されていれば、OKです。


2. 基本的な認証機能を追加

usernameとpasswordで特定のページにアクセス出来るような認証機能を追加しましょう


2.1 view関係のファイルを追加・編集

今回利用するのは「views/index.jade,views/layout.jade,views/login.jade」の3つです。

login.jadeはデフォルトでは存在しないので、新規でファイルを作成してください。


views/layout.jade

doctype html

html
head
title= title
meta(name='viewport', content='width=device-width, initial-scale=1.0')
link(href='http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', rel='stylesheet', media='screen')
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content

script(src='http://code.jquery.com/jquery.js')
script(src='http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js')



views/index.jade

extends layout

block content
if (!user)
a(href="/login") Login
br

if (user)
p こんにちは #{user} さん
a(href="/logout") Logout



views/login.jade

extends layout

block content
.container
h1 Login Page
p.lead デモ用のログインページです
br
form(role='form', action="/login",method="post", style='max-width: 300px;')
.form-group
input.form-control(type='text', name="username", placeholder='Enter Username')
.form-group
input.form-control(type='password', name="password", placeholder='Password')
button.btn.btn-default(type='submit') Submit
 
a(href='/')
button.btn.btn-primary(type="button") Cancel



2.2 適切なルートを設定

「routes/index.js」を編集して、ルートの設定を行います。

コメントで追記する部分、編集する部分を記載していますので、確認しつつ、追記・編集してください。


index.js

var express = require('express');

var router = express.Router();

var passport = require('passport'); // 追記

router.get('/', function(req, res, next) {
res.render('index', { user : req.user}); // 編集
});

// 以下追記=========================================================
router.get('/login', function(req, res) {
res.render('login', { user : req.user });
});

router.post('/login', passport.authenticate('local',
{successRedirect: '/',
failureRedirect: '/login',
session: false}));

router.get('/logout', function(req, res) {
req.logout();
res.redirect('/');
});
// ここまで========================================================

module.exports = router;


上記のコードの説明を少し記載します。

router.post('/login',...の部分で、認証に関するルートの設定を行っています。

適切に認証された場合は、「/」にリダイレクトし、認証が失敗した場合は、「/login」にリダイレクトするような設定です。

具体的にどのようなルールで認証しているのかは、次のapp.jsで設定します。


app.js

var createError = require('http-errors');

var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var passport = require('passport'); // 追記
var LocalStrategy = require('passport-local').Strategy; // 追記

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

var app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 追記ここから====================================================
app.use(passport.initialize());
var LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
passReqToCallback: true,
session: false,
}, function (req, username, password, done) {
process.nextTick(function () {
if (username === "test" && password === "test") {
return done(null, username)
} else {
console.log("login error")
return done(null, false, { message: 'パスワードが正しくありません。' })
}
})
}));
// 追記ここまで====================================================

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;


上記のコードの説明を軽く記載します。

今回は簡略化のため、usernameがtestでpasswordがtestの場合のみ認証を成功させるようにしています。

もう少し凝った実装にする場合は、データベースと連携することで、登録されたusername,passwordかどうかで認証をすることが出来ます。

認証が成功した場合は、doneメソッドでpassportに対して認証成功を伝えます。

ちなみに、usernameFieldとpasswordFieldは、login.jadeのform-controlのnameと一致させる必要があります。

※注意点として、passportの設定を書く位置が重要です。

ルートの設定である以下のコードよりも下にpassportの設定を書くとエラーになるので、気をつけてください。

// これよりも上にpassprtの設定を書くこと

app.use('/', indexRouter);
app.use('/users', usersRouter);


2.3 動作確認

「localhost:3000/login」にアクセスしてください。

usernameとpasswordにそれぞれ「aaa」を入力し、「submit」を押してください。

認証が失敗するので、ログインページにリダイレクトされるかと思います。

次に、usernameとpasswordにそれぞれ「test」を入力し、「submit」ボタンを押してください。

認証が成功し、indexページにリダイレクトされるかと思います。

この実装によって、usernameとpasswordでユーザーを認証し、認証の可否によってルートを変更することが出来るようになりました。

しかし、このままでは実際の場面での利用は厳しいです。

通常の利用では、セッション単位でログイン情報を保持する必要がありますが、この実装ではログイン情報を保持しません。

なので、リダイレクト先では、認証されたかどうかが分からないということです。


3. セッション情報を保持した認証機能を追加

セッション情報を保持することで、リダイレクト先でも認証されたかどうかが分かります。

これにより、認証された人専用のページを表示する事ができます。

編集・追記する部分にはコメントを挿入していますので、そちらを参考に編集・追記を行ってください。


3.1 ソースコード


index.js

var express = require('express');

var router = express.Router();

var passport = require('passport');

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

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

router.post('/login', passport.authenticate('local',
{successRedirect: '/',
failureRedirect: '/login',
session: true})); // 編集

router.get('/logout', function(req, res) {
req.logout();
res.redirect('/');
});

module.exports = router;


上記のindex.jsでは、sessionを有効にするために、session属性をtrueに設定しています。


app.js

var createError = require('http-errors');

var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var session = require('express-session'); // 追記

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

var app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// セッションミドルウェア設定
app.use(session({ resave:false,saveUninitialized:false, secret: 'passport test' })); // 追記

app.use(passport.initialize());
app.use(passport.session()); // 追記
var LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
passReqToCallback: true,
session: false,
}, function (req, username, password, done) {
process.nextTick(function () {
if (username === "test" && password === "test") {
return done(null, username)
} else {
console.log("login error")
return done(null, false, { message: 'パスワードが正しくありません。' })
}
})
}));

// 以下追記=================================================
passport.serializeUser(function (user, done) {
done(null, user);
});

passport.deserializeUser(function (user, done) {
done(null, user);
});
// ここまで追記===============================================

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

module.exports = app;


あまり変更点は無いのですが、シリアライズ・デシリアライズについて少し記載します。

コードで言うと「passport.serializeUser」「passport.deserializeUser」の部分です。


3.2 シリアライズ・デシリアライズ

まず、シリアライズとは何かを説明します。

簡単に言うと、データをファイルとして保存できる形式に変換することを、シリアライズといいます。

通常、オブジェクトデータはオブジェクトグラフと呼ばれるデータ構造になっており、一つを保存すれば良い、とはならないようです。

リンク状になったデータを保存できるように直列に繋ぐ作業を、シリアライズという(らしい)。

シリアライズの流れを以下に記載します。

「passport.use(new LocalStrategy({...」内の「return done(null, username)」で、usernameが「passport.serializeUser」関数のコールバック関数の第一引数に渡ります。

今回は、userという引数内にusernameが渡ることになります。

デシリアライズは、逆にシリアライズした情報をプログラムで処理できるように解凍する作業です。

passport.serializeUser(function (user, done) {

done(null, user); // ← この関数でシリアライズして保存される
});

Passportのシリアライズ作業はセッションごとに一回しか行われません。

今回の場合は、ログイン処理を行った際に1回のみ行われます。

デシリアライズ処理は、セッションが維持されている間、アクセスがあるたびに毎回行われます。

1__node_と_Preferences_と_localhost_3000.png

ちなみに、デシリアライズした値は、req.userで取り出すことが出来ます。

シリアライズしたデータはreq.userに保存されています。


index.js

...

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


確認

usernameにtestを、passwordにtestをそれぞれ入力して、submitボタンを押して下さい。

以下の図のようになるはずです。

localhost_3000.png

このページをリロードしても、同じ表示のままです。

これは、セッションが維持されているからです。

新しくGETリクエストを送る際に、Cookie値にセッションIDを含めて送っています。

localhost_3000.png

セッションが維持されていることを確認するために、ブラウザでクッキーを確認します。

connect.sidという名前のクッキーが保存されていることがわかります。

ちなみに、コンテンツの部分がセッションIDの本体です。

img123.png

ためしに、curlコマンドでもアクセスしてみましょう。

$ curl -b connect.sid=セッションID http://localhost:3000/

// 今回の場合は以下のようになる
$ curl -b connect.sid=s%3A-VOjUj2cqBN... http://localhost:3000/

下記のような返答があれば、成功です。

つまり、他の人のセッションIDを盗めば、その人専用のページを見ることが出来るということですね。

これをセッションハイジャックと呼ぶそうです。

...

<body><p>こんにちは test さん</p><a href="/logout">Logout</a></body>
...


参考サイト

大変参考にさせて頂きました。

ありがとうございます。

Passport

[Java]難解なSerializableという仕様について俺が知っていること、というか俺の理解

node.js+express+passport.jsでlocal認証を試したメモ

User Authentication with Passport and Express 4