📱

QA効率化: JWTで実現するNFCタグ・QRコードのリダイレクトシステム

に公開

TL;DR

「NFCタグやQRコードでリダイレクトしたいけど、URLを直接埋め込むのは変更できなくて不便...」「URLを短縮サービスに頼るのはセキュリティ的に心配...」

そんな悩み、JWTで解決しましょう。

Cloudflare WorkersとHonoを使ったたった33行のコードで、署名付きリダイレクトURLを管理できます。NFCタグにURLを書き込んだ後でも、リダイレクト先を自由に変更可能。しかもエッジで超高速。

// こんなにシンプル
app.get('/r', async (c) => {
  const token = c.req.query('t')
  const payload = await verify(token, c.env.JWT_SECRET)
  return c.redirect(payload.url as string)
})

きっかけ:テスト環境ごとのURL、開くの面倒だな問題

普段はWebエンジニアとして開発したり、QAエンジニアとしてテストしたりしています。

iPhoneでWebアプリのテスト実施をしていたある日、こんな状況に遭遇しました。

「テスト環境ごとのURLを毎回ブックマークから開くの面倒だな...」

注記: 本記事で紹介するシステムは、筆者がプライベートで開発したシステムのQA業務において使用しています。勤務先企業のQA業務では使用していません。

NFCタグやQRコードって便利ですよね。スマホをかざすだけ、読み取るだけで、対象のテスト環境にアクセスできます。

でも、大きな問題があります。

短縮URLサービスという選択肢の問題点

「じゃあ、bit.lyとか短縮URLサービス使えばいいじゃん?」

確かに。でも、いくつか問題があります:

  • 外部サービスへの依存: サービスが終了したら?
  • セキュリティの懸念: 第三者のサービスを信頼できるか?
  • 情報流出の懸念: 自社のテスト環境のURLを外部サービスに保存してもいいのか?
  • コスト: 大量のURLを管理すると料金が...

「自前で安全なリダイレクトシステムを作れないかな...」

NFCタグの「紛失したらアクセスし放題」問題

NFCタグにURLを書き込むと、基本的にそれで固定されます。

  • NFCタグを落としてしまったら? → テスト環境のURL変更

これ、めちゃくちゃ危険じゃないですか?

そう思ったのが、このプロジェクトの始まりでした。

このシステムでの対策

NFCタグ自体の紛失リスクに対して、このシステムでは以下の対策が可能です:

  • NFCタグ紛失時: 新しいトークンを発行し、古いトークンの有効期限が切れるまで待つ
  • 即座に無効化したい場合: JWT_SECRETを変更することで全トークンを無効化(ただし正規ユーザーも再発行が必要)
  • 個別管理が必要な場合: ブラックリスト方式を併用(別途実装が必要)

有効期限を設定しておくことで、紛失時のリスクを最小限に抑えられます。

JWT(JSON Web Token)ってなんだ?

JWTを選んだ理由

まず、「なぜJWTなのか?」という話から。

従来のリダイレクトシステムなら、こんな構成になります:

1. データベースにURL情報を保存
2. 短縮IDを生成(例: abc123)
3. リクエストが来たらDBから検索
4. リダイレクト先を取得して転送

でも、これってデータベースが必須なんです。

そこでJWTです。JWTを使うと:

1. リダイレクト先URLをトークンに埋め込む
2. トークンに署名して改ざん防止
3. リクエストが来たら署名を検証
4. トークン内のURLへリダイレクト

データベース不要! しかも改ざんされないから安全。

JWTの仕組みを超シンプルに説明

JWTは、以下の3つの部分から構成される文字列です:

ヘッダー.ペイロード.署名

具体的な例:

eyJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2V4YW1wbGUuY29tIn0.署名部分

1. ヘッダー(Header)

{
  "alg": "HS256",  // 使用する署名アルゴリズム
  "typ": "JWT"     // トークンのタイプ
}

2. ペイロード(Payload)

{
  "url": "https://example.com/destination"  // リダイレクト先URL
}

ここが重要! リダイレクト先のURLを直接トークンに埋め込みます。

3. 署名(Signature)

// 秘密鍵を使って署名を生成
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

この署名があるおかげで、誰かがトークンを改ざんしようとしても検知できます

なぜJWTが安全なのか

// 悪意のある人がトークンを改ざんしようとしたら...
const tamperedToken = token.replace('example.com', 'malicious.com')

// 署名検証で弾かれる!
await verify(tamperedToken, SECRET) // → エラー!

秘密鍵(SECRET)を知らない限り、正しい署名を作れません。だから、改ざんされても検証の段階で検知できるんです。

実装:たった33行のリダイレクトシステム

使用技術スタック

  • Hono - 超軽量なWebフレームワーク(Express的な)
  • Cloudflare Workers - エッジで動くサーバーレス環境
  • JWT - トークンベースの署名・検証
  • TypeScript - 型安全な開発

プロジェクト構成

nfc-tag-redirect-manager/
├── src/
│   └── index.ts           # メインのリダイレクト処理(33行)
├── generate-url.ts        # 署名付きURL生成ツール(40行)
├── wrangler.jsonc         # Cloudflare Workers設定
└── package.json

シンプルすぎる構成。 これだけで動きます。

メインコード(src/index.ts)

import { Hono } from 'hono'
import { verify } from 'hono/jwt'

type Bindings = {
  JWT_SECRET: string
}

const app = new Hono<{ Bindings: Bindings }>()

app.get('/r', async (c) => {
  // 1. 環境変数から秘密鍵を取得
  const SECRET = c.env.JWT_SECRET

  if (!SECRET) {
    return c.text('JWT_SECRET not configured', 500)
  }

  // 2. クエリパラメータからトークンを取得
  const token = c.req.query('t')

  if (!token) {
    return c.text('Token required', 401)
  }

  try {
    // 3. トークンを検証してペイロードを取得
    const payload = await verify(token, SECRET)

    // 4. ペイロード内のURLへリダイレクト
    return c.redirect(payload.url as string)
  } catch (error) {
    // 5. 検証失敗 = 改ざんされている
    return c.text('Invalid token', 403)
  }
})

export default app

たったこれだけ。 33行で安全なリダイレクトシステムの完成です。

コードの流れを詳しく解説

ステップ1: 秘密鍵の取得

const SECRET = c.env.JWT_SECRET

Cloudflare Workersの環境変数から秘密鍵を取得します。この秘密鍵はトークンの署名検証に必須です。

本番環境では、以下のコマンドで安全に設定:

wrangler secret put JWT_SECRET

ステップ2: トークンの取得

const token = c.req.query('t')

URLは https://your-worker.workers.dev/r?t=<トークン> という形式。
クエリパラメータ t からトークンを取り出します。

ステップ3: 署名の検証

const payload = await verify(token, SECRET)

verify()関数が以下を実行:

  1. トークンの署名部分を秘密鍵で検証
  2. 改ざんされていなければペイロードを返す
  3. 改ざんされていれば例外を投げる

ステップ4: リダイレクト実行

return c.redirect(payload.url as string)

検証に成功したら、ペイロードに含まれるURLへリダイレクト。

署名付きURL生成ツール(generate-url.ts)

リダイレクトを受け取る側のコードだけでは意味がありません。
署名付きURLを生成するツールも必要です。

import { sign } from 'hono/jwt'

const SECRET = process.env.JWT_SECRET
const BASE_URL = process.env.BASE_URL

const destination = process.argv[2]

async function generateSignedUrl() {
  // 1. リダイレクト先URLをペイロードとしてトークン生成
  const token = await sign({ url: destination }, SECRET)

  // 2. ベースURLにトークンを付けて完成
  const signedUrl = `${BASE_URL}/r?t=${token}`

  console.log('✅ Signed URL generated:')
  console.log(signedUrl)
}

generateSignedUrl()

使用方法

# 環境変数を設定
export JWT_SECRET="your-secret-key-here"
export BASE_URL="https://your-worker.workers.dev"

# 署名付きURLを生成(実践的な例)
# QA環境へのアクセスURL生成
npx tsx generate-url.ts https://qa-staging.example.com

# 特定フィーチャーブランチの環境
npx tsx generate-url.ts https://feature-xyz.qa.example.com

出力:

✅ Signed URL generated:
https://your-worker.workers.dev/r?t=eyJhbGciOiJIUzI1NiJ9...

このURLをNFCタグやQRコードに埋め込むだけ!

セキュリティ面での工夫

1. 改ざん検知

// もし誰かがトークンを書き換えようとしたら...
try {
  await verify(tamperedToken, SECRET)
} catch (error) {
  return c.text('Invalid token', 403)  // 即座に拒否
}

2. 秘密鍵の安全な管理

# ❌ コードにハードコードしない
const SECRET = "my-secret-key"

# ✅ CloudFlare上の環境変数で管理
wrangler secret put JWT_SECRET

3. HTTPSの強制

Cloudflare Workersは全てHTTPSで配信されます。通信経路も暗号化されているので安心。

4. トークンの有効期限(オプション)

今回の実装には含めていませんが、必要に応じて有効期限も設定できます:

// 生成時に有効期限を設定
const token = await sign(
  {
    url: destination,
    exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30  // 30日間有効
  },
  SECRET
)

// 検証時に自動でチェックされる
const payload = await verify(token, SECRET)  // 期限切れならエラー

デプロイと運用

1. Cloudflare Workersへのデプロイ

# 1. wrangler CLI のインストール(初回のみ)
npm install -g wrangler

# 2. Cloudflareにログイン
wrangler login

# 3. 秘密鍵の設定
wrangler secret put JWT_SECRET
# プロンプトでランダムな長い文字列を入力

# 4. デプロイ
npm run deploy

これだけ。 10秒程度でデプロイされます。

2. カスタムドメインの設定

Cloudflareのダッシュボードから、自分のドメインを設定できます:

your-redirect.yourdomain.com → https://your-worker.workers.dev

3. 運用コスト

Cloudflare Workersの無料枠:

  • 100,000 リクエスト/日
  • CPU時間 10ms/リクエスト

このシンプルなリダイレクトなら、ほぼ無料で運用できます。

JWTを使用するメリット・デメリット

メリット

  1. データベース不要

    • DBの構築・管理が不要
    • サーバーレスで完結
    • メンテナンスコストが低い
  2. ステートレス

    • サーバー側で状態管理しない
    • スケーラビリティが高い
    • 複数サーバーでも問題なし
  3. 改ざん検知

    • 署名で改ざんを防止
    • 中間者攻撃にも強い
  4. シンプル

    • コードが短い(33行!)
    • 理解しやすい
    • バグが少ない

デメリット

  1. トークンの無効化が難しい

    • 発行済みトークンは基本的に取り消せない
    • 対策:有効期限を短くする
    • 対策:ブラックリスト管理(別途実装必要)
  2. トークンサイズ

    • URLが長くなりがち
    • NFCタグの容量制限に注意
    • QRコードは問題なし
  3. リダイレクト先の変更が不可

    • トークン発行後は変更不可
    • 対策:新しいトークンを再発行する
    • 対策:用途ごとにトークンを発行

どんな時に使うべきか

JWTが向いているケース

  • NFCタグ・QRコードの固定URL
  • リダイレクト先が頻繁に変わらない
  • シンプルさ・低コストを優先
  • セキュリティが必要

JWTが向いていないケース

  • リダイレクト先を頻繁に変更したい
  • 発行済みURLを即座に無効化したい
  • 条件分岐が複雑

→ この場合はDB管理の短縮URLシステムの方が適切

実際のQA業務での活用例

具体的にどんな場面で使えるのか、実例を紹介します。

ケース1: 複数のテスト環境を素早く切り替え

デスクにNFCタグを3つ配置:

  • タグA: 開発環境 (dev)
  • タグB: ステージング環境 (staging)
  • タグC: QA専用環境 (qa)

iPhoneをかざすだけで、ブックマークを探す手間が省けます。テストケース実行の度に環境を切り替える必要がある場合、大幅な時短になります。

ケース2: QRコードでテスト手順書に埋め込み

テストケースごとに異なる初期状態のURLを生成し、NotionやConfluenceなどのテスト手順書にQRコードとして貼り付け。

# テストケース#1: 新規ユーザー登録画面
npx tsx generate-url.ts https://qa.example.com/signup

# テストケース#2: ログイン済み状態のダッシュボード
npx tsx generate-url.ts https://qa.example.com/dashboard?auth=test-token

テスト実施者はQRコードを読み取るだけで、正確な初期状態から検証を開始できます。

ケース3: 実機検証の効率化

10台のiPhone実機でブラウザテストする際、各端末でURLを手入力する代わりに、

  • NFCタグを準備して全端末で読み取り
  • QRコードを画面に表示して一斉読み取り

このように複数デバイスのテストで威力を発揮します。

推奨するNFCタグ

実際に使用しているNFCタグの情報です:

使用製品: 中華製の防水NFCタグ (NTAG215) - Amazon

NFCタグ選定のポイント

  • 容量: 最低144バイト(NTAG213以上)を推奨
  • 理由: JWTトークンは長くなりがち(200-400文字程度)
  • 推奨モデル: NTAG213, NTAG215, NTAG216

NTAG213は144バイトの容量があり、一般的なJWTトークンであれば問題なく書き込めます。より長いトークン(有効期限やその他のメタデータを含む場合)を使う場合は、NTAG215(504バイト)やNTAG216(888バイト)を選びましょう。
今回はNTAG215(504バイト)を買いました。

よくあるトラブルと対処法

トークンが長すぎてNFCタグに書き込めない

原因: ペイロードに不要なデータが含まれている、またはNFCタグの容量不足

対処法:

  • ペイロードを最小限に(urlのみ)
  • 容量の大きいNFCタグ(NTAG215以上)を使用
  • リダイレクト先URLを短縮(パスパラメータを削減)

Invalid tokenエラーが出る

原因: 署名検証の失敗

対処法:

  • JWT_SECRETが生成時と検証時で一致しているか確認
  • トークンがコピペ時に改行や空白が入っていないか確認
  • トークンの有効期限が切れていないか確認

NFCタグが読み取れない

原因: iPhoneの読み取り位置がずれている、またはNFCタグの不良

対処法:

  • iPhoneの画面上部(カメラ付近)にNFCタグを近づける
  • NFCタグが金属面に貼られている場合は、金属から離す
  • NFCタグ自体を別のものに交換してみる

まとめ:シンプルさの中に安全性を

このプロジェクトで学んだこと:

技術面

  • JWTの仕組み: 署名によるデータの保護
  • Honoの軽量さ: Expressより圧倒的にシンプル
  • Cloudflare Workersの手軽さ: デプロイまでが高速

設計面

  • シンプルさの価値: たった33行でも実用的
  • ステートレスの強み: スケーラビリティと運用コスト
  • 適材適所: JWTが全てじゃない、使いどころを見極める

同じ悩みを持つ人へ

もし「テスト環境のURLも検証対象のモバイル端末も多い...」と思っているなら、このシステムを試してみてください。

重要: 業務で利用する場合は、必ず管理者や上長に確認をとってください。本記事で紹介したシステムは、筆者のプライベートプロジェクトでの使用を前提としています。


リポジトリ: nfc-tag-redirect-manager

同じ悩みを持つ人の助けになれば嬉しいです。

Discussion