まやかしの認証で始めるサインインのイロハのイ ~ Passport.js, ミドルウェア, CoR パターン ~
プログラム自学案内の 39 回目です。今回は、Web アプリにサインインの仕組みを Passport.js を使って付け足します(ただし、ちゃんとした認証は後回しです)。 これまでの記事は次のリンクから辿ってください。
Passport.js の導入
Passport.js ( https://www.passportjs.org/ )を使うと、Express.js アプリにサインインの仕組みを入れることができます。本職のプログラマーは例によってPassport.js の公式サイトの取説を読みながら、使い方を探っていくわけですが、これちょっと難易度が高いので、この記事ですこしずつ、ガイドしたいと思います。
まずは プロジェクトに Passport.js を追加します。
npm install passport
npm install passport-local
ミドルウェアって?
ところで、Passport.js のページの冒頭には、「Passport は Node.js のための認証ミドルウェアである」、と書かれています。
ミドルウェア(middleware) という言葉には、一般には「OS とアプリケーションの間で動くソフトウェア」といった意味を持ちますが、Express.js などのフレームワークを用いた Web アプリ開発の文脈では 全く違う意味を持つ ので注意が必要です。
Express.js アプリにおけるミドルウェアは、HTTP リクエストを受け取り、HTTP レスポンスを返す小さなソフトウェア部品のことを指します。
ミドルウェアは、Web サーバに届いた全ての Path に対する HTTP リクエストを、ルーティングが行われるまえに受けとります。
ミドルウェアは複数のものを、数珠つなぎにして使うことができます。
個々のミドルウェアは、特定の責務のみ(「認証認可」「エラー処理」「ログ出力」など)を引き受けてリクエストを処理し、それ以外の責務については、後続のミドルウェアにたらい回しにします。
ミドルウェアの数珠つなぎの末尾には、ルータが位置します。ここまで HTTP リクエストがたどり着くと、ルーティングが行われ、Path ごとの処理が呼び出されます。
「Path」「ルーティング」「ルータ」ってなんだっけ? という方は以下の記事を確認してみてください。
じつは、上の記事で紹介しているコードでも、ミドルウェアがいくつも数珠つなぎになっているのが確認できます。
app.js(抜粋)
const app = express();
app.engine("html", mustacheExpress());
app.set("views", __dirname + "/views");
// app.use(ミドルウェア) で 数珠つなぎにミドルウェアを追加
app.use(logger("dev")); // ミドルウェアを追加
app.use(express.json()); // ミドルウェアを追加
app.use(express.urlencoded({ extended: false })); // ミドルウェアを追加
app.use(express.static(__dirname + "/static")); // ミドルウェアを追加
app.use("/", router); // 末尾にルータを追加
app.listen(3000, () => {
console.log("Listening on port 3000...");
});
Chain-of-responsibility pattern
「いろんな役割の小さな部品を数珠つなぎにして、各部品で処理しきれないものは後続にたらい回しにする」というのはなかなか巧妙なやり方なので、ミドルウェア以外でも、ときおり目にすることがあります。
このやり方は Chain-of-responsibility pattern (CoR パターン) という名前で呼ばれています。覚えておいて損はないでしょう。日本語名は残念ながらありませんが、あえて訳すなら「数珠つなぎ責任者方式」といったところでしょうか。
まずはサインイン画面を作る
ではさっそく、サインイン画面を作ってみましょう。このセクションではまず、ただの Web フォームを作ります。まだ Passport.js は使いません。
やることは基本的に、以前(23 回目)の記事で紹介した内容ですので、分からないところがあれば、以前の記事を参考に読んでみてください。
サインイン画面へのリンクを作る
メインの画面にリンクを一行追加します。
views/list_view.html(抜粋)
<body>
<h1> Hello flatter-web !!! </h1>
<a href="/sign-in">サインイン</a> <!-- 追加 -->
サインイン画面を作る
以下のファイルを追加します。
views/sign_in.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/stylesheets/style.css">
<title> flatter-web サインイン</title>
</head>
<body>
<h1> flatter-web サインイン </h1>
<form action="sign-in/enter" method="post">
なまえ:
<input name="username" type="text" required autofocus>
<br>
パスワード(なにかしら入れてください):
<input name="password" type="password">
<br>
<button type="submit">サインイン</button>
</form>
</body>
</html>
ルーティングを設定
views/sign_in.html 画面のリンクやフォームで追加した二つのリクエストパス、sign(GET), sign-in/enter(POST) に対するルーティングを加えます。
controllers/router.js(抜粋)
router.get('/sign-in', function (req, res, next) {
res.render('sign_in.html');
});
router.post('/sign-in/enter', function (req, res, next) {
res.redirect('/');
});
以上で、サインイン画面が出来ました。一度これで Web アプリを起動して、サインイン画面を表示させて、動きを確認してみてください。 まだ、サインインボタンを押しても何も起きないと思います。
まやかしの認証機能の追加
つぎにいよいよ、passport をつかった認証を追加します。ちゃんとした認証は難易度が高いので、ここでは、まやかしの認証機能を追加します。
passport による認証機能本体
auth/auth.jsというファイルを作って、以下の内容をコピペし保存してください。そして、コードをなんとなく眺めて、ふーんと思ってください。
auth/auth.js
const passport = require("passport");
const LocalStrategy = require("passport-local");
const strategy = new LocalStrategy(async (username, password, cb) => {
if (password === "NG") {
return cb(null, false);
} else {
return cb(null, username);
}
});
passport.use("local", strategy);
passport.serializeUser(async (user, cb) => {
return cb(null, user);
});
passport.deserializeUser(async (user, cb) => {
return cb(null, user);
});
function sign_in({ successRedirect, failureRedirect }) {
return passport.authenticate("local", {
successRedirect: successRedirect,
failureRedirect: failureRedirect,
});
}
function sign_out({ successRedirect }) {
return function (req, res, next) {
req.logout(function (err) {
if (err) {
return next(err);
}
res.redirect(successRedirect);
});
};
}
const session_authenticate = passport.authenticate("session");
module.exports = {
sign_in: sign_in,
sign_out: sign_out,
session_authenticate: session_authenticate,
};
コードを読んでいただければ、勘づかれると思いますが、このコードではパスワードに"NG"以外の文字列が入っていれば、サインインに成功します。まやかしと言ったゆえんです。いったんはこんなもんでいいでしょう。
セッションを利用したユーザの情報取得処理を追加
auth/auth.js に準備した、ミドルウェア「session_authenticate」を、Express.jsアプリに追加します。これにより、一度サインインすればあとはセッションがつづく限り、後続のルータでサインインしたユーザの情報を取得できるようになります。取得の仕方は後述します。
app.js(抜粋)
const router = require("./controllers/router");
const auth = require("./auth/auth"); //追加
const app = express();
app.engine("html", mustacheExpress());
app.set("views", __dirname + "/views");
app.use(logger("dev"));
app.use(
session({
secret: "a random set of characters like 1aq2ws3de4",
resave: false,
saveUninitialized: true,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(__dirname + "/static"));
app.use(auth.session_authenticate); //ここにミドルウェアを追加
app.use("/", router);
サインイン / サインアウト の ルーティングを設定
サインイン / サインアウトの リクエストパスと、auth/auth.jsに書いたサインイン、サインアウトの処理をルーティングで紐づけます。
controllers/router.js(抜粋)
const auth = require("../auth/auth");
//(中略)
router.get('/sign-in', function (req, res, next) {
res.render('sign_in.html');
});
router.post('/sign-in/enter', auth.sign_in({
successRedirect: "/", failureRedirect: "/sign-in"
}));
router.post('/sign-out', auth.sign_out({
successRedirect: "/"
}));
サインインしたユーザ名と、サインアウトリンクを表示
さいごに、ユーザ名をメイン画面に表示します。先ほど追加したsession_authenticateミドルウェアは、ユーザ情報を req.user に設定します。ミドルウェアの後続にあるルータではこのユーザ情報を利用できますので、ユーザ情報を読み取って画面に渡します。
controllers/router.js(抜粋)
router.get('/', function (req, res, next) {
const data = namelist();
data.username = req.user // サインインしたユーザを取得し画面に渡す
data.history = req.session.history
res.render('list_view.html', data);
});
メイン画面では、ユーザ名を表示するとともに、サインアウトのボタンを追加します。
views/list_view.html(抜粋)
<body>
<h1> Hello flatter-web !!! </h1>
{{^username}}
<a href="/sign-in">サインイン</a>
{{/username}}
{{#username}}
ようこそ{{username}}さん
<form action="sign-out" method="post">
<button type="submit">サインアウト</button>
</form>
{{/username}}
以上がおわりましたら、再度 Web サーバを起動してみてください。サインインとサインアウトが出来るようになったはずです。ただ、パスワードに"NG"以外の何を入れてもサインインできてしまう、という欠点はありますが。
まとめと次回予告
今回の記事では、passport.js ライブラリを紹介し、サインイン画面の作り方を紹介しました。あわせて、これまで紹介しそびれていた概念、Express.js
におけるミドルウェアについても紹介しました。
次回は、「利用者ごとに情報を保管する」「特定の利用者にのみ機能を許可(認可)」しくみを作ります。