Tips
PHPでQRコードを生成する前に決めること
2026-01-09
この記事が解決する状況
| あなたの状況 | 読むべきセクション |
|---|---|
| 印刷物にQRコードを載せる予定 | QRコードに何を埋め込むべきか |
| キャンペーンQRを作りたい | ID方式のメリット |
| QRのアクセス数を計測したい | サーバー側の処理例 |
| セキュリティが気になる | IDの設計 / セキュリティチェックリスト |
| とにかく実装方法を知りたい | ライブラリ選定 → 実装パターン |
「URLを直書きしたQRを印刷物に載せたことがある」なら、リスクを理解しておくべき。
よくある失敗
「QRコードにURL直書きしたら、キャンペーン終了後もアクセスが来て困った」
印刷物に載せたQRは回収できない。URLを変えたくなっても、QR自体は変わらない。
この手の事故は設計段階で防げる。
QRコードに「何を埋め込むべきか」
URL直埋めのリスク
| 問題 | 具体例 |
|---|---|
| 変更不可 | キャンペーンURLを差し替えたい → 印刷済みQRは変更不可 |
| 失効不可 | 退会した会員のQRが生き続ける |
| 追跡困難 | どのQRからのアクセスか判別できない |
| 漏洩リスク | URL内のパラメータがリファラ経由で外部に漏れる |
結論: URL直埋めは「二度と変更しない」確信があるときだけ。
ID方式のメリット
QRには識別子だけを埋め込み、意味付けはサーバー側で行う。
QR内容: https://example.com/q/A1B2C3D4
└─ IDだけ。行き先はサーバーが決める
| 要件 | URL直埋め | ID方式 |
|---|---|---|
| リダイレクト先の変更 | ✗ | ✓ |
| QRの失効・無効化 | ✗ | ✓ |
| アクセス解析・トラッキング | △ | ✓ |
| 有効期限の設定 | ✗ | ✓ |
IDの設計:推測されない・衝突しない
避けるべきパターン
// ❌ 連番 → 推測可能
$id = $memberId; // 1, 2, 3...
// ❌ タイムスタンプのみ → 衝突リスク
$id = time();
会員ID 1234 のQRを見た攻撃者が 1235 を試す。これで他人の情報にアクセスできたら事故。
推奨パターン
// ✓ UUID v4(ランダム)
$id = bin2hex(random_bytes(16));
// → "a3f8c9e1b2d4f6a8c0e2d4f6a8b0c2e4"
// ✓ HMAC署名付き(改ざん検知可能)
$payload = $memberId . '|' . $expiry;
$signature = hash_hmac('sha256', $payload, $secretKey);
$id = base64url_encode($payload . '|' . $signature);
| 方式 | 推測耐性 | 改ざん検知 | 用途 |
|---|---|---|---|
| UUID v4 | ✓ | ✗ | 一般的な識別子 |
| HMAC署名 | ✓ | ✓ | 有効期限・権限を含める場合 |
セキュリティ:攻撃ベクトルと対策
QRコードを悪用した攻撃パターン
| 攻撃 | 手口 | 対策 |
|---|---|---|
| ID推測 | 連番を順番に試す | UUID v4を使う |
| QRすり替え | 正規QRの上に偽QRを貼る | ドメインを目視確認(ユーザー教育) |
| URL注入 | 入力値をそのままQRに | サニタイズ必須 |
| リファラ漏洩 | リダイレクト先でURL内パラメータが漏れる | Referrer-Policyヘッダー |
| 期限切れQR悪用 | 古いQRが再利用される | 有効期限チェック |
入力値のサニタイズ
ユーザー入力をQRに含める場合は必ずサニタイズ。
// ❌ 危険:入力値をそのまま使用
$content = $_POST['url'];
$qr = (new QRCode())->render($content);
// ✓ 安全:バリデーション + サニタイズ
$url = filter_var($_POST['url'], FILTER_VALIDATE_URL);
if (!$url) {
throw new InvalidArgumentException('Invalid URL');
}
// さらにドメインをホワイトリストでチェック
$allowedDomains = ['example.com', 'sub.example.com'];
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowedDomains, true)) {
throw new InvalidArgumentException('Domain not allowed');
}
$qr = (new QRCode())->render($url);
サーバー側の処理例
// /q/[id] へのアクセスを処理
<?php
$id = $_GET['id'] ?? '';
// バリデーション
if (!preg_match('/^[a-zA-Z0-9]{32}$/', $id)) {
http_response_code(400);
exit('Invalid ID format');
}
// DBから取得
$stmt = $pdo->prepare('SELECT * FROM qr_codes WHERE id = ? AND expires_at > NOW()');
$stmt->execute([$id]);
$record = $stmt->fetch();
if (!$record) {
http_response_code(404);
exit('このQRコードは無効です');
}
// アクセスログ記録
$pdo->prepare('INSERT INTO qr_access_logs (qr_id, accessed_at, ip, user_agent) VALUES (?, NOW(), ?, ?)')
->execute([$id, $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '']);
// リファラ漏洩対策
header('Referrer-Policy: no-referrer');
// リダイレクト
header('Location: ' . $record['destination_url']);
exit;
これで「QRの失効」「アクセス解析」「リダイレクト先変更」がすべて可能になる。
ライブラリ選定
| ライブラリ | 特徴 | 判断 |
|---|---|---|
| chillerlan/php-qrcode | 軽量・依存少・現行メンテ | ✓ 採用 |
| endroid/qr-code | Symfony向け・機能豊富 | フレームワーク利用時 |
| Google Chart API | 外部依存・非推奨 | ✗ 使わない |
なぜ chillerlan/php-qrcode か:
- 外部APIに依存しない(オフライン生成可能)
- 依存パッケージが少ない
- PHP 7.4〜8.x 対応、アクティブにメンテされている
composer require chillerlan/php-qrcode
PHP 7.4以上、ext-gd または ext-imagick が必要。
実装パターン
基本:PNG出力
<?php
require_once 'vendor/autoload.php';
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
$id = 'A1B2C3D4'; // DBから取得したID
$options = new QROptions([
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
'eccLevel' => QRCode::ECC_M,
'scale' => 10,
]);
header('Content-Type: image/png');
header('Cache-Control: no-store');
echo (new QRCode($options))->render('https://example.com/q/' . $id);
Base64埋め込み(HTML直書き)
<?php
$options = new QROptions([
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
'imageBase64' => true,
'scale' => 8,
]);
$base64 = (new QRCode($options))->render('https://example.com/q/' . $id);
?>
<img src="<?= $base64 ?>" alt="QR Code">
外部ファイル不要。メール埋め込みやPDF生成に向く。
SVG出力(印刷向け)
<?php
$options = new QROptions([
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
'svgViewBoxSize' => 100,
]);
header('Content-Type: image/svg+xml');
echo (new QRCode($options))->render('https://example.com/q/' . $id);
拡大しても劣化しない。印刷物・PDF埋め込みに最適。
オプション早見表
| オプション | 説明 | 推奨値 |
|---|---|---|
eccLevel | 誤り訂正レベル | 通常 ECC_M、ロゴ重ね ECC_H |
scale | 1モジュールのピクセル数 | 8〜12 |
imageBase64 | Base64出力 | HTML埋め込み時 true |
eccLevel の選び方:
ECC_L(7%): データ量優先ECC_M(15%): 通常用途ECC_H(30%): ロゴ重ね・汚損リスクあり
セキュリティチェックリスト
| 項目 | 確認 |
|---|---|
| IDは推測困難か | UUID v4 または HMAC署名 |
| 有効期限はあるか | DBに expires_at カラム |
| 失効処理は可能か | 管理画面から無効化できる |
| アクセスログは取れるか | 不正利用の検知に必要 |
| リファラ漏洩対策 | Referrer-Policy: no-referrer ヘッダー |
| 入力値サニタイズ | ユーザー入力をQRに含める場合 |
| IDフォーマット検証 | 不正な形式のIDを弾く |
まとめ
- QRにはURLではなくIDだけを埋め込む
- IDは推測不可能な形式(UUID v4 / HMAC)
- 失効・変更・トラッキングはサーバー側で制御
- ライブラリは
chillerlan/php-qrcode一択 - 入力値サニタイズとリファラ漏洩対策を忘れずに
関連記事
- PHPで日時切り替えを実装する前に確認すること — キャンペーン期間の表示制御
- お問い合わせフォームが届かない原因の特定方法 — QRからのお問い合わせが届かない時