更新日: 2024年10月16日


II. QRコードで本人確認する自治会会員証

1. プログラム作成の背景

「I. 人生の余技として作成したプログラム」の「7-1. VBA」で書いた通り、駄待ち狐は仕事をリタイアしてから自治会に関っています。この○○○自治会では活動の一環として11月に「○○○まつり」を開催します。ストラックアウトやダーツのゲームコーナー、輪投げやヨーヨー釣りの夜店、焼きそばや唐揚げの屋台が出る本格的なものですが、全ての世話役は住民有志の実行委員会です。有料のものは金券払いですが、自治会費から補助金も出しているので自治会員と非会員で金券の販売価格を分けるべきだという話になりました。350世帯1,000人規模の自治会なので、顔を見れば会員かどうか分るという訳には行きません。

そこで会員証を作るという話になるのですが、その条件は厳しいものです。

普通の会員証は最初の2つの条件がない前提で偽造しにくいものにして、その結果3番目の条件は満たされます。しかし、まつりの金券販売で非会員を割高にするための会員証発行に費用がかかっては本末転倒です。普通のプリンタで簡単に印刷できるのに、コピーしても使えないというのはなかなかの難問です。さらにまつりの主役は子供たちなので4番目の条件も重要です。子供が会員証を落すことはままあることですし、それで叱られては可哀そうです。ここまでの条件を満たそうとすると、金券販売時に会員証を見せることに加えてのチェックが必要になりますが、それに手間暇がかかるのは困るというのが5番目の条件です。

2. 解決策の概要

上記の5条件を満たすように考え出したのが以下に示す会員証です。A4の紙1枚に8枚の会員証を印刷します。会員証に記載されているQRコードは世帯ごとに違います。どの世帯のものかは各会員証の右下に書かれた班の番号と世帯主名で分るのですが、この部分は各世帯に配布する時に切取ってしまいます。世帯の人数分だけ同じQRコードが印刷されていて、左上の佐藤太郎さんは2人家族なのでその右も同じQRコードです。左の上から2番目の鈴木春子さんは単身世帯で1枚だけ、その右の高橋夏子さんは3人家族なので3枚あるといった具合です。この印刷用PDFファイルを生成するVBAは後ほど紹介します。

画像を新しいタブで開くと高解像度で見ることができます。

会員証のQRコード、例えば左上のものをスマホで読取ると

https://bidingfox.xii.jp/qr-id_php/check.php?id=5383692040

となっています。最後の10桁の数字以外はどのQRコードも共通で、当サイト内に設けたデモ用の会員証チェックファイル/qr-id_php/check.phpにリンクしています。このPHPファイルについても後ほど説明しますが、アクセスするとユーザ名とパスワードを訊かれます。一般の人はここでストップですが、まつりのスタッフは

ユーザ名:   bidingfox
パスワード: damachikitsune

を知っているので、これを入力してログインするとスマホの画面に以下が表示されます。

この画面は見せずに会員証を示した人に「お名前と何班かを言って下さい」と訊いて「1班の佐藤花子です」と回答があれば本人確認は完了です。ユーザ名とパスワードはスマホが記憶してくれるので、毎回入力する必要はありません。世帯ごとに10桁の数字は違っていて、そのQRコードに対応する家族全員の名前が表示されます。

1,000人分の会員証を作成するにはA4の紙125枚を印刷してカットする必要がありますが、印刷用のPDFファイルはVBAで自動生成されるので工数はまあまあリーズナブルです。不正使用に関しては、拾った会員証が誰のものか分りませんし(10桁の数字を家族名に変換できるのはスタッフだけ)、鈴木春子さんが非会員の子供に会員証をあげたとしても子供が一人暮しというのはおかしいので不正が分ります。本人確認についてもスタッフが自分のスマホを使えばいいので簡単ですし、表示は高齢のスタッフでも見やすい大きな文字です。

本サイトの冒頭に書いた「必要とされる機能をその時点で利用できるリソースで最も効率的に実現する」という点について補足しておきます。上記の方法で完全に不正使用が防げるとは言えませんが、最初に掲げた5条件はクリアできていると思います。一方、このシステムで利用したリソースは以下の通りです。

上の2つは駄待ち狐が支払っているMicrosoft Officeとさくらインターネットの月額利用料に依存しています。Officeについては割高感が否めませんが、ExcelとAdobe Photoshopは30年来の愛用アプリなので、サブスクになっても、年金生活になっても利用を続けています。一方、さくらインターネットの割安感は特筆に値すると思います。下の2つは社会の趨勢ということになります。QRコードのアイデア自体も素晴しいですが、これだけ普及したのはスマホのカメラで読取るという使い方があってのことでしょう。何れにせよ、自力でも他力でもその時点で利用できるリソースを駆使するというのが駄待ち狐のモットーです。

3. VBAプログラム

VBAを仕込んだExcelファイルqr-id_vba.xlsmをHTML形式で保存したqr-id_vba.htmlをまず紹介します。Excelと同様にSheet1~Sheet3が選択できるようになっていますが、HTML形式で保存すると行列番号が表示されず、セルに埋込まれている関数も見られなくなるので、Sheet1は元のExcelファイルを加工してこれらを表示しています。E列に班名、F列以降に家族の名前が4行(4世帯分)書かれています。実際には350世帯分を自治体で管理している住民台帳から転記しますが、仮名で例示しています。

A~D列は10桁の乱数を生成するためのものです。A列の関数で生成される乱数はExcelを操作するごとに変ってしまうので、最終版をD列に数値でコピーします。B列とC列は異なる世帯に同じ番号が割当てられていないことをチェックするためのものです。10桁の乱数を350個生成した時に同じ番号が発生する確率は16.4万回に1回(Pythonプログラムを自作して出した数字)ですが、ゼロでない限り確認しておく必要があります。B列でA列の乱数をソートし、B列で上下の行が等しい時はC列に1が立つようにして、C1セルにその合計を表示しています。C1が0なら重複なしです。これで準備は整ったので、以下に示すマクロの出番です。

コードの表示にはPrism.jsを使用しています。
prism.cssの設定は団塊爺ちゃんの備忘録を参考にしました。

Option Explicit

Sub サーバ保存用ファイルを作成()
	Dim i As Integer, imax As Integer  'Sheet1の行カウンタとその最大値
	Dim j As Integer, jmax As Integer  '各行に並ぶ名前のカウンタと個数
	Dim fn As String  '作成するファイル名

	'「サーバ保存用ファイル」フォルダがなければ作成する
	If Dir(ThisWorkbook.Path & "\サーバ保存用ファイル", vbDirectory) = "" Then
		MkDir ThisWorkbook.Path & "\サーバ保存用ファイル"
	End If
	'記入された行数分のPDFファイルを生成する
	imax = Cells(Rows.Count, 4).End(xlUp).row
	For i = 1 To imax
		Worksheets("Sheet2").Cells.ClearContents
		Worksheets("Sheet2").Cells(1, 1).Value = Cells(i, 5).Value & " 班"
		'各行で横に並ぶ名前をSheet2の2列目に縦に並べる
		jmax = Cells(i, Columns.Count).End(xlToLeft).Column - 5
		For j = 1 To jmax
			Worksheets("Sheet2").Cells(j, 2).Value = Cells(i, j + 5).Value
		Next j
		'Cells(i, 4)の数値を10桁の文字列にしてファイル名にする
		fn = Format(Cells(i, 4).Value, "0000000000")
		'Sheet2をPDFファイルにして「サーバ保存用ファイル」フォルダに保存する
		Worksheets("Sheet2").ExportAsFixedFormat Type:=xlTypePDF, _
			Filename:=ThisWorkbook.Path & "\サーバ保存用ファイル\" & fn & ".pdf"
	Next i
End Sub

Sub 会員証PDFファイルを作成()
	Dim i As Integer, imax As Integer    'Sheet1の行カウンタとその最大値
	Dim j As Integer, jmax As Integer    '各行に並ぶ名前のカウンタと個数
	Dim page As Integer, pos As Integer  '会員証シートの枚数カウンタと会員証の配置カウンタ
	Dim url As String  'QRコードにするurl
	Dim pn As String   'QRコードのpngファイル名
	Dim shp As Shape

	'「IDのQRコード」フォルダがなければ作成する
	If Dir(ThisWorkbook.Path & "\IDのQRコード", vbDirectory) = "" Then
		MkDir ThisWorkbook.Path & "\IDのQRコード"
	End If
	'「会員証PDFファイル」フォルダがなければ作成する
	If Dir(ThisWorkbook.Path & "\会員証PDFファイル", vbDirectory) = "" Then
		MkDir ThisWorkbook.Path & "\会員証PDFファイル"
	End If
	'Sheet3のQRコードと「配布用の班と名前」を削除する
	clearSheet3
	page = 1
	pos = 0
	'記入された行数(imax)分のQRコード会員証を生成する
	imax = Cells(Rows.Count, 4).End(xlUp).row
	For i = 1 To imax
		pn = "QR" & Format(i, "000") & ".png"
		'Cells(i, 5)の数値を10桁の文字列にしてurlの最期に付加する
		url = "https://bidingfox.xii.jp/qr-id_php/check.php?id=" & Format(Cells(i, 4).Value, "0000000000")
		'APIでQRコードのpngファイルを作成する
		makeQRC url, ThisWorkbook.Path & "\IDのQRコード\" & pn
		'各行で横に並ぶ名前の数だけ会員証を生成する
		jmax = Cells(i, Columns.Count).End(xlToLeft).Column - 5
		For j = 1 To jmax
			'QRコードを挿入する
			With Worksheets("Sheet3").Pictures.Insert(ThisWorkbook.Path & "\IDのQRコード\" & pn)
				.ShapeRange.LockAspectRatio = msoTrue
				.Height = 175
				.Top = 200 + 474 * (pos \ 2)
				.Left = 170 + 650 * (pos Mod 2)
			End With
			'配布用の班と名前を記入する
			With Worksheets("Sheet3")
				.Cells(13 + 16 * (pos \ 2), 18 + 20 * (pos Mod 2)).Value = Cells(i, 5).Value
				.Cells(14 + 16 * (pos \ 2), 16 + 20 * (pos Mod 2)).Value = Cells(i, 6).Value
			End With
			If pos = 7 Then
				Worksheets("Sheet3").ExportAsFixedFormat Type:=xlTypePDF, _
					Filename:=ThisWorkbook.Path & "\会員証PDFファイル\QRC会員証_" & page & ".pdf"
				'Sheet3のQRコードと「配布用の班と名前」を削除する
				clearSheet3
				page = page + 1
				pos = 0
			Else
				pos = pos + 1
			End If
		Next j
	Next i
	'途中で終っている最後のページをPDF保存する
	If pos <> 0 Then
		Worksheets("Sheet3").ExportAsFixedFormat Type:=xlTypePDF, _
			Filename:=ThisWorkbook.Path & "\会員証PDFファイル\QRC会員証_" & page & ".pdf"
	End If
End Sub

Private Sub clearSheet3()
	Dim shp As Shape
	Dim pos As Integer

	For Each shp In Worksheets("Sheet3").Shapes
		If shp.Type = msoLinkedPicture Then shp.Delete
	Next shp
	For pos = 0 To 7
		With Worksheets("Sheet3")
			.Cells(13 + 16 * (pos \ 2), 18 + 20 * (pos Mod 2)).Value = ""
			.Cells(14 + 16 * (pos \ 2), 16 + 20 * (pos Mod 2)).Value = ""
		End With
		Next pos
End Sub

Private Sub makeQRC(url As String, ppath As String)
	Dim http As New MSXML2.XMLHTTP60
	Dim temByte() As Byte

	http.Open "GET", "https://api.qrserver.com/v1/create-qr-code/?data=" & url & _
		"& size=100x100 & ecc=M", False
	http.send Null
	temByte() = http.responseBody
	Open ppath For Binary Access Write As #1
	Put #1, , temByte()
	Close #1
End Sub

3行目からの"Sub サーバ保存用ファイルを作成()"ですが、サーバ保存用ファイルとはスマホに表示される世帯ごとの家族名を記したPDFファイルで、ファイル名は各世帯に割当てられた10桁の乱数です。世帯数分のPDFファイルをサーバに保存し、スマホのブラウザで該当のPDFを開きます。350世帯分の家族名をデータベース(DB)としてサーバ上に置き、PHPプログラムで各世帯を表示する方がエレガントですが、DB化してしまうと逆に個人情報漏洩の危険性が高まるのではないかと考えました。Sheet2上に世帯ごとのPDF原稿を作成しては「サーバ保存用ファイル」フォルダに保存するということを世帯数分だけ繰返します。

「サーバ保存用ファイル」フォルダがない場合は、9~11行目でqr-id_vba.xlsmと同じフォルダ内に新たに作成します。13行目でSheet1の4列目(10桁の乱数の最終版が記入されたD列)の行数(世帯数)をカウントしてimaxとします。14行目からのForループはこの行数分だけ各行ごとに処理をします。15行目でSheet2をクリアした後、Cells(1, 1)(A1)セルに班名、Cells(j, 2)(B列)の1行目から縦に家族名を並べていきます。10桁の乱数の先頭に0がある場合、標準の表示形式では桁数が減ってしまうので、23行目で先頭に0を付けてファイル名にします。25~26行目でSheet2をPDFで保存すれば各行ごとの処理は完了です。

30行目の"Sub 会員証PDFファイルを作成()"でいよいよ会員証PDFファイルを作成します。ここでのポイントは世帯ごとに(10桁の乱数部分が)異なるQRコードを生成することです。これは107行目の"Private Sub makeQRC(...)"で行いますが、実際のところは111行目にあるコマンドで"api.qrserver.com"が提供してくれているAPIにお任せです。「I. 人生の余技として作成したプログラム」の「3. C」で書いた"nkf"のように、誰もが欲しいと思うものは往々にして誰かが準備してくれています。生成されたQRコードは"QRxxx.png"(xxxは3桁の通し番号)というファイル名で「IDのQRコード」フォルダに保存します。

QRコードの生成がクリアできれば、Sheet3にある会員証のひな形でQRコードを差替えます。まず、47行目でSheet3のQRコードと配布用の班と名前を削除しますが、この処理はもう一度出てくるので"clearSheet3()"というPrivate Sub(92行目)にしてあります。Sheet3にあるShapesでタイプがmsoLinkedPictureのものはQRコードだけなので、96~98行目でこれを全て削除します。99~104行目では班と名前が記入されたセルの位置を指定して、その値を""にしています。この後、62~67行目でQRコードを挿入しますが、この際には位置の指定が必要になります。69~72行目で班と名前を記入するセルは削除した場所と同じです。

各行で横に並ぶ名前の数だけ会員証を生成するForループ(60~83行目)の途中であっても、posでカウントしている会員証の配置位置(0~7)が7になると、74~77行目でSheet3をPDF保存してclearSheet3を実行します。この場合はposカウンタをリセットしますが、それ以外の場合はカウントアップします。世帯の人数が8で割切れない場合は86~89行目で最後のページをPDF保存します。「1,000人分、125個のPDFファイルができるの?!」と仰る向きには、CubePDF Pageをお薦めします。あるいは、Excelファイル上でSheet3を書換えるのではなく、どんどんシートを増やして行くという方法もあります。

4. Apacheによる認証(失敗)

今までの説明から、察しのいい方は「さくらのレンタルサーバ上にユーザ名/パスワードで保護されたディレクトリを作って『サーバ保存用ファイル』を入れるんだな」ということがお分りだと思います。ここで問題になるのが認証方法です。(これから説明する「ベーシック認証」と「ベーシック認証にフォーム入力を使う」は結果としてうまく行かなかったので、成功例だけを知りたい方は読み飛ばして下さい。)Apacheで一番簡単な認証方法はパスワード保護したいディレクトリに以下の.htaccessファイルを置き、暗号化したパスワードが書かれた.htpasswdファイルを指定するというベーシック認証です。

AuthType Basic
AuthName "Restricted Files"
AuthBasicProvider file
AuthUserFile (サーバ上の絶対パス)/.htpasswd
Require valid-user

ところが、このベーシック認証はiPhoneでアクセスした時に落し穴があります。入力したパスワードをiPhoneが記憶してくれないのです。正確に言うと、iPhoneの『設定』にある『パスワード』がパスワードを記憶するかどうかを訊いてくれません。ブラウザのSafariは取敢えずパスワードを覚えてくれるのですが、半日もすると忘れてしまいます。『設定』の『パスワード』に手動でパスワードを記憶させるという裏技もあるのですが、パスワードを自動入力する際にどのサイトのパスワードを使うかをリストの中から選択する必要があります。これでは「金券販売の際の本人確認が面倒でないこと」という条件が満たせなくなってしまいます。

この問題はフォーム認証にすれば解決すると思い、「ベーシック認証にフォーム入力を使う」を参考にして、デュアルブートパソコンのUbuntu上に構築したApache環境でテストしました。まず、パスワード保護されていないディレクトリに以下のlogin.htmlと.htaccessファイルを置きます。login.htmlはユーザ名とパスワードを入力するフォームを表示するHTMLです。フォームのaction先である"dologin.html"はファイルとしては存在しませんが、.htaccessの<Files>で同じファイル名を指定することでアクセス制御し、ログインが成功するとパスワード保護されたディレクトリ内にあるindex.htmlが表示されます。


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>ログイン</title>
</head>

<body>
<h3>
会員証のQRコードに紐付いた情報を見るためにはログインして下さい。
</h3>
<form method="POST" action="dologin.html" style="font-size: large;">
<p>ユーザ名:
<input type="text" name="httpd_username" value="">
</p>
<p>パスワード:
<input type="password" name="httpd_password" value="">
</p>
<p>
<button type="submit" name="login" value="Login" style="font-size: large;">ログイン</button>
</p>
</form>
</body>
</html>
<Files dologin.html>  # login.htmlでアクション先として指定したファイル名
    AuthType form
    AuthName "Restricted Files"
    AuthFormProvider file
    AuthUserFile (サーバ上の絶対パス)/.htpasswd
    SetHandler form-login-handler
    AuthFormLoginRequiredLocation (非保護webディレクトリ)/login.html
    AuthFormLoginSuccessLocation (保護webディレクトリ)/index.html
    Session On
    SessionMaxAge 0
    SessionCookieName session path=/
    SessionCryptoPassphrase (セッション暗号化パスフレーズ)
</Files>

一方、パスワード保護したいディレクトリには以下のindex.htmlと.htaccessファイルを置きます。ログイン後に表示すべきファイルは世帯ごとの家族名を記したPDFですが、まずは動作確認のために"It works!"を表示するだけのindex.htmlにしています。「ベーシック認証にフォーム入力を使う」ではindex.htmlにログアウト用のボタンを作成し、.htaccessにログアウト時の処理を記述することになっていますが、今回の会員証の使い方ではログアウトする必要がないので省略しました。上記と下記の.htaccessで、SessionCookieNameとSessionCryptoPassphraseには同じものを設定します。


<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>テストページ</title>
</head>

<body>
<h1>It works!</h1>
</body>
</html>
AuthType form
AuthName "Restricted Files"
AuthFormProvider file
AuthUserFile (サーバ上の絶対パス)/.htpasswd
Require valid-user
AuthFormLoginRequiredLocation (非保護webディレクトリ)/login.html
Session On
SessionCookieName session path=/
SessionCryptoPassphrase (セッション暗号化パスフレーズ)

以上で準備は整ったと思い、ブラウザでlogin.htmlにアクセスすると"Internal Server Error"が表示されます。これはUbuntuのapache2の設定でmod_auth_formが有効になっていなかったためでした。ターミナルで

$ a2enmod auth_form
$ sudo service apache2 restart

を実行するとログインフォームが表示されるようになり、ログインすると"It works!"が表示されました。iPhoneでアクセスしてもパスワードを記憶するかどうか訊いてくれます。

さくらのレンタルサーバでも動作するかどうか確認するため、上記4ファイルをアップロードしてテストすると、"Internal Server Error"が表示されます。レンタルサーバでもmod_auth_formが有効になっていないようです。そこでカスタマーセンターに連絡です。3週間にわたるメールのやり取りの結果、「本件はApple側の実装の問題」であり「mod_auth_formの有効化につきましては・・・作業工程数が相応になってしまう事もあり、具体的な見通しは立っていない状態」との最終回答でした。「mod_auth_formの有効化につきまして・・・現在のところ、松田様以外からのご要望は確認しておりません」とも言われました。

ここまで言われては仕方ないので、フォーム認証のプログラムを自作するしかありません。これについれは次項で述べます。ただ、一般論としては自作プログラムよりもApacheに組込まれたモジュールを使う方が安全性は高いと思います。mod_auth_formの公式サイトには「フォーム認証はHTTPクッキーを利用するmod_sessionモジュールに依存するため、クロスサイトスクリプティング攻撃によって個人情報が流出する可能性がある」旨の警告がありますが、自作のPHPプログラムでもクッキーに基づくセッションを使用するので、このリスクは同じです。

5. PHPプログラムによる認証

「認証プログラムのサンプルはないかな、PerlよりもPHPがいいな」とグーグっていると、「団塊爺ちゃんの備忘録」の中にある「ログイン画面の作成」が見つかったので参考にさせていただきました。このサイトのコード表示もおしゃれだったので、prism.cssの設定も見習わせていただきました。団塊爺ちゃんさん、本当にありがとうございます。本題のプログラムですが、サーバ上の/qr-id_phpディレクトリにcheck.phpとlogin.phpを置き、/qr-id_php/pdf_filesディレクトリに「3. VBAプログラム」で作成したサーバ保存用ファイルをアップロードします。check.phpは以下のようになっています。


<?php
$id_num = $_GET['id'];
// セッションを開始(セッション変数を利用できる)
session_start();
// ログインしていなければログインページに移動
if(!isset($_SESSION['user'])){
    header("Location: ./login.php?id=".$id_num);
    exit();
}
// 指定のpdfファイルを表示
$file_path = $_SERVER['DOCUMENT_ROOT']."/qr-id_php/pdf_files/".$id_num.".pdf";
$file_size = filesize($file_path);
header("Content-Type: application/pdf");
header("Content-Length: $file_size");
readfile($file_path);
exit();
?>

会員証のQRコードに記載された

https://bidingfox.xii.jp/qr-id_php/check.php?id=5383692040

でアクセスすると、クエリストリングで渡される10桁の乱数を$id_numとします。6行目ではセッションに'user'がセットされているかどうかでログインを判断し、ログインしていなければ7行目でlogin.phpに移動します。この時も"?id=".$id_numで10桁の乱数を引継ぎます。ログインしていれば11~15行目でpdf_filesディレクトリにあるPDFファイル(10桁の乱数がファイル名)を読取って表示します。pdf_filesには以下の.htaccessを置いてwebアクセスを禁止しますが、サーバ上のPHPプログラムからはファイルを読出せます。

Require all denied

一方、login.phpは以下のようになっています。40~51行目のHTMLでログインフォームを表示しますが、"username"と"password"を入力して"loginbtn"をクリックすると6~25行目のPHPプログラムが実行されます。8~9行目ではフォーム入力された"username"と"password"を$userと$passにします。10~11行目ではwebアクセス禁止のpdf_filesディレクトリに置かれたusername.txtとpassword.txtから読取った内容を$usernameと$passwordにします。"$user == $username && $pass == $password"であれば認証成功なので、セッションの'user'に$userをセットしてcheck.phpに移動します。


<?php
$id_num = $_GET['id'];
session_start();
$errorMessage = "";
// ログインのリクエストがあったとき
if(isset($_REQUEST['loginbtn'])){
    $id_num = $_REQUEST['id_num'];
    $user = $_REQUEST['username'];
    $pass = $_REQUEST['password'];
    $username = file_get_contents($_SERVER['DOCUMENT_ROOT']."/qr-id_php/pdf_files/username.txt");
    $password = file_get_contents($_SERVER['DOCUMENT_ROOT']."/qr-id_php/pdf_files/password.txt");
    // ユーザー名とパスワードの確認
    if($user == $username && $pass == $password){
		// 認証成功
        session_regenerate_id(true);
        $_SESSION['user'] = $user;
        // チェックページに移動
        header("Location: ./check.php?id=".$id_num);
        exit();
	}else{
		// 認証失敗
        session_destroy();
        $errorMessage = "ユーザー名、パスワードのいずれかに誤りがあります。";
	}
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン</title>
</head>

<body>
<h3>
会員証のQRコードに紐付いた情報を見るためにはログインして下さい。
</h3>
<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
<input type="hidden" name="id_num" value="<?php echo $id_num ?>">
<p>ユーザー名:
<input type="text" name="username">
</p>
<p>パスワード:
<input type="password" name="password">
</p>
<p>
<input type="submit" name="loginbtn" value="ログイン" style="font-size: large;">
</p>
</form>
<h3>
<?php echo $errorMessage ?>
</h3>
</body>
</html>

2行目の$id_numと4行目の$errorMessageについて補足しておきます。$id_numはcheck.phpから渡された10桁の乱数ですが、認証が成功してcheck.phpに戻る18行目の処理でcheck.phpに返します。QRコードからアクセスされた時、check.phpはクエリストリングで10桁の乱数を受取るので、認証成功後にlogin.phpから戻る際にも同様にする必要があります。一方、初期値が""の$errorMessageは認証が失敗した時に23行目で設定されます。この$errorMessageは53行目で認証失敗後のログイン画面に表示されます。


本サイトでこの後に紹介するプログラムは作成年が古いものから順番に並べてありますが、ここで紹介した「QRコードで本人確認する自治会会員証」は最新作です。プログラムを作成した背景と解決策が一般の(特定の技術に精通していない)方にも分りやすく、「必要とされる機能をその時点で利用できるリソースで最も効率的に実現する」というコンセプトを理解していただくのに最適だと思ったからです。また、最新であるがゆえに「自力でも他力でもその時点で利用できるリソースを駆使する」という考え方も実感してもらえるのではないかと思いました。

ただ、この自治会会員証は実際には使われることはありませんでした。自治会員かどうかで値段が違うという仕組みが子供には理解しがたく、不公平感を与えるのではないかという配慮からです。ではどうしたかというと、会員限定の前売り券を当日販売する券よりも安くして、払戻しありにすることで会員には前売り券を買ってもらうことにしたのです。駄待ち狐としては、この結論に不満はありません。プログラミングは十分に楽しませてもらえたし、一生懸命やったものが実用化・事業化に至らないというのは駄待ち狐の人生あるあるだからです。サイト冒頭に書いた「雌伏50年で未だ伏せたまま」とはこのことです。