📲 第十一章:LIFF SDK 初期化とLine非表示ログイン機構
前章までで、純粋なウェブページのレイアウトとデプロイについて説明しました。しかし現代のビジネスアプリケーションにおいて、最も経営者と顧客を悩ませるのが「ユーザー登録転換率」です。 もし勤怠システムで、毎回従業員が「アカウント、パスワード」を入力しなければならないとしたら、従業員は不平を言い、毎日パスワードを忘れるでしょう。
この問題を解決するため、台湾で最もシェアの高いLineは究極の武器を提供しています:LIFF (LINE Front-end Framework)。 あなたのウェブサイトをLineのチャットルーム内にマウントして開くと、Lineは「このユーザーのLine IDとプロフィール画像」をあなたのウェブサイトにこっそり送信します。 ユーザーは一切文字を入力する必要なく、瞬時にログインが完了します!
この授業では理論は扱わず、実際に運用されている勤怠システムのソースコードを直接解剖します。 Vibe Codingの方法で、AIに指揮をとらせ、この非常に複雑で落とし穴だらけの初期化と認証フローを書く方法を教えます!
🔑 実践1:ReactでLIFF SDKを導入・初期化する
LIFFはnpm installで簡単にインストールできるようなパッケージではありません。
Line APP内蔵ブラウザとの低レベル通信に関わるため、その初期化時間(liff.init())は非同期であり、初期化に失敗するとウェブページ全体がクラッシュします。
💡 Vibe Prompt 実践1:安全なLIFF初期化Hookの作成
初期化ロジックを適当なボタンの中に書くことはできません。アプリケーション全体が「Lineの準備はできたか?」を常に把握できるように、グローバルなカスタムHook(useLiff)が必要です。
[!IMPORTANT] 以下のPromptをAIにコピーして送信してください:
Line LIFFウェブアプリケーション(React)を開発中です。カスタムHook:useLiff()を作成してください。要件:1. @line/liffパッケージを使用。2. 状態管理:isReady(デフォルトfalse)、liffError(デフォルトnull)、profile(ユーザーのLineデータ、デフォルトnull)。3. useEffect内でliff.init({ liffId: 'あなたのLIFF_ID' })を実行。4. 初期化成功時、liff.isLoggedIn()をチェック。ログイン済みの場合、liff.getProfile()でユーザーのプロフィール画像と名前を取得し、profile状態に保存。5. 未ログインの場合、liff.login()で強制ログイン。完全なコードと、catchブロックに詳細なエラーハンドリングを追加してください。
🤖 AIが生成した企業レベルのLIFF初期化Hook:
import { useState, useEffect } from 'react';
import liff from '@line/liff';
export function useLiff() {
const [isReady, setIsReady] = useState(false);
const [liffError, setLiffError] = useState(null);
const [profile, setProfile] = useState(null);
useEffect(() => {
// 非同期関数で初期化を実行
const initLiff = async () => {
try {
console.log("⏳ LIFF SDK初期化開始...");
// 1. SDK初期化 (LIFF_IDは環境変数に置くこと)
await liff.init({ liffId: import.meta.env.VITE_LIFF_ID });
console.log("✅ LIFF初期化成功!");
// 2. Lineログイン状態をチェック
// (Line APP内で開いた場合、通常ここはtrue)
if (liff.isLoggedIn()) {
console.log("✅ ユーザーログイン済み、プロフィール取得中...");
const userProfile = await liff.getProfile();
setProfile(userProfile);
console.log("👋 ようこそ:", userProfile.displayName);
} else {
// 3. 外部ブラウザで未ログインの場合、Lineログインページにリダイレクト
console.log("⚠️ ユーザー未ログイン、リダイレクト準備...");
liff.login();
}
// 4. 全て完了、Ready状態に
setIsReady(true);
} catch (error) {
console.error("❌ LIFF初期化で致命的エラー:", error);
setLiffError(error.message);
}
};
// 初期化実行
initLiff();
}, []); // 空配列でページロード時1回のみ実行
// 状態を外部コンポーネントに提供
return { isReady, liffError, profile };
}
🔍 詳細解析:
これはLineボット開発者の悪夢ですが、AIが完璧に処理してくれました。
ここには極めて重要なメカニズムがあります:liff.login()。
このURLを上司に送信し、上司がPCのChromeブラウザで開いた場合(Line内でない場合)、liff.isLoggedIn()はfalseになります。
この時、このコードは非常に賢く自動的にLineのグリーンのログイン画面にリダイレクトし、上司にアカウントとパスワードの入力を要求します。これで「Line内蔵ブラウザ」と「外部ブラウザ」の両方のシナリオを完璧にカバーします!
🚫 究極のトラブルシューティング:LIFF初期化の無限ループ
これは10万円相当のデバッグ経験です!
多くの初心者が公式ドキュメント通りにliff.login()を書いた後、デプロイすると発見します:外部ブラウザで開いた場合、「無限ログインループ」に陥り、ページが点滅し続けます!
なぜか?liff.login()を呼び出すと、Lineはページを認証センターにリダイレクトし、認証後にページを「再読み込み」します。
もしURL設定が間違っていると、Lineは正しくURLに認証パラメータを付加できず、ページは「まだログインしていない」と判断し、再びliff.login()を呼び出します。
💡 Vibe Prompt 実践2:AIにLIFFコールバックURLをチェックさせる
[!IMPORTANT] 以下のPromptをAIにコピーして送信してください:
Reactページで外部ブラウザからliff.login()を呼び出すと、無限リロードのループが発生します。これはLIFF Consoleの"Endpoint URL"設定に問題があるのでしょうか?Vercel(URLはhttps://my-punch.vercel.app/)にデプロイする場合、Line DevelopersコンソールのEndpoint URLには何を設定すべきですか?また、このバグを防ぐため、liff.login()の構文をredirectUriパラメータを明示的に指定するように修正してください。
🤖 AIの緊急停止対策:
これは間違いなくコールバックURLが一致していないことによる無限ループです!
【Line Developersコンソール設定チェック】:
Endpoint URLには正確に記入する必要があります:`https://my-punch.vercel.app/`
(⚠️ 注意:React RouterのHashモードを使用している場合、/#/が付くと大惨事になります。Lineはデフォルトで#以降を切り捨てます!)
【コードの安全修正】:
liffに任せるのではなく、明示的に`redirectUri`を指定します:
// 修正前の危険な書き方:
// liff.login();
// ✅ 修正後の安全な書き方:
liff.login({
// Lineに認証後にユーザーを戻すURLを明示
redirectUri: window.location.href
});
🔐 実践3:身元の二重認証 (JWT Token)
さて、ユーザーのprofile(名前とプロフィール画像を含む)を取得しました。この名前をそのままバックエンド(FastAPI)に送り、「王小明さんが打刻しました!」と伝えれば良いのでしょうか?
絶対にダメです!これは最も深刻なセキュリティホールです!
もしバックエンドAPIが「名前」だけを認証する場合、ハッカーは偽のウェブページを作成し、「社長」という名前をあなたのバックエンドに送信するだけで、バックエンドは社長が打刻したと認識します! 実際のビジネスシステムでは、フロントエンドからバックエンドに送信するのは平文の名前ではなく、**「偽造不可能な通行証(ID Token)」**でなければなりません。
�� Vibe Prompt 実践3:LineのJWT Tokenを取得する
[!IMPORTANT] 以下のPromptをAIにコピーして送信してください:
LIFFアプリケーションで、ユーザーの打刻データをバックエンドサーバー(FastAPI)に送信します。ハッカーによる身元偽造を防ぐため、liff.getProfile()で取得した名前だけを送信することはできません。安全なJWT認証を取得するため、liff.getIDToken()の使用方法を教えてください。punchIn()関数を作成し、Tokenを取得し、fetch()でHTTP Header(Authorization: Bearer)に載せてバックエンドAPI(/api/punch)に送信する例を示してください。詳細なセキュリティコメントを付加してください。
🤖 AIの企業レベルの偽造防止打刻機能:
export const punchIn = async () => {
try {
// 1. Lineが発行する安全な通行証(JWT Token)を取得
// この文字列はLineサーバーによって暗号化署名されており、ハッカーは同じTokenを偽造できません
const idToken = liff.getIDToken();
if (!idToken) {
throw new Error("安全な認証を取得できません、再ログインしてください!");
}
console.log("🔐 通行証を取得し打刻準備...");
// 2. APIを叩きバックエンドにデータ送信
const response = await fetch('https://あなたのバックエンドURL.com/api/punch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 🔥 ここが肝!通行証をHeaderに載せて送信
'Authorization': `Bearer ${idToken}`
},
body: JSON.stringify({
action: 'clock_in',
timestamp: new Date().toISOString()
// ⚠️ 注意:名前やLine IDをバックエンドに送信する必要は全くありません!
// バックエンドはTokenを受け取ると、Lineサーバーに「このTokenは誰のものか?」を問い合わせます
})
});
const result = await response.json();
if (response.ok) {
alert("✅ 打刻成功!");
} else {
alert("❌ 打刻失敗:" + result.message);
}
} catch (error) {
console.error("打刻で例外発生:", error);
alert("システム異常、管理者に連絡してください。");
}
};
🔍 詳細解析:
これがこのシステムが数十万円の価値がある理由です。
**「ゼロトラストアーキテクチャ(Zero Trust)」**は現代SaaSシステムの標準装備です。フロントエンドは極めて不安全で、誰でもブラウザ内のデー���を改ざんできます。
しかしliff.getIDToken()が生成する乱数文字列には、Lineサーバーのデジタル署名が含まれています。あなたのバックエンドはこの署名を解読することで、「このリクエストは確かに王小明本人のLine APPから送信されたもの」と100%確信できます。これが非表示ログインの最高峰です!
⏳ 実践4:LIFF状態と画面の完璧な連携
Hookと打刻機能を作成したので、これらをウェブページの画面に適用します。 LIFFの準備が整う前、画面はどうあるべきか? 外部ブラウザで開いてリダイレクトされた場合、画面はちらつかないか?
💡 Vibe Prompt 実践4:体験満載のローディング画面(Splash Screen)作成
APP開発では通常、起動画面(Splash Screen)があります。 ReactとTailwindで実装します。
[!IMPORTANT] 以下のPromptをAIにコピーして送信してください:
App.jsxで先ほど作成したuseLiff() hookを使用します。要件:1. const { isReady, liffError, profile } = useLiff();を呼び出し。2. liffErrorがある場合、全面赤のエラー画面を表示。3. isReadyがfalseの場合、ダークテイストの全面Loading画面(回転アイコンと"システム初期化中..."テキストを含む)を表示。4. isReadyがtrueの場合のみ、メイン画面(ユーザーのアバター、名前、「出勤打刻」ボタンを含む)を表示。完全なApp.jsxコードを提供してください。
🤖 AIの高品質アプリケーションエントリ:
import React from 'react';
import { useLiff } from './hooks/useLiff';
import { punchIn } from './api'; // 先ほど作成した打刻関数
export default function App() {
const { isReady, liffError, profile } = useLiff();
// ケース1:致命的エラー発生(LIFF ID未設定など)
if (liffError) {
return (
<div className="min-h-screen bg-zinc-900 flex items-center justify-center p-6">
<div className="bg-red-500/20 border border-red-500 text-red-100 p-6 rounded-2xl max-w-sm text-center">
<h2 className="text-xl font-bold mb-2">⚠️ システムロード失敗</h2>
<p className="text-sm opacity-80">{liffError}</p>
</div>
</div>
);
}
// ケース2:Lineサーバーと接続中(Splash Screen)
if (!isReady) {
return (
<div className="min-h-screen bg-zinc-900 flex flex-col items-center justify-center">
{/* Tailwindのanimate-spinで回転 */}
<div className="w-12 h-12 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin mb-4"></div>
<p className="text-emerald-500 font-bold tracking-widest animate-pulse">
システム初期化中...
</p>
</div>
);
}
// ケース3:接続成功!メイン画面表示
return (
<div className="min-h-screen bg-zinc-900 text-white p-6">
<header className="flex items-center gap-4 mb-10">
{/* Lineアバター表示 */}
{profile?.pictureUrl ? (
<img
src={profile.pictureUrl}
alt="avatar"
className="w-16 h-16 rounded-full border-2 border-emerald-500 shadow-lg shadow-emerald-500/20"
/>
) : (
<div className="w-16 h-16 rounded-full bg-zinc-800 border-2 border-zinc-700"></div>
)}
<div>
<p className="text-zinc-400 text-sm">おはようございます、お疲れ様です</p>
<h1 className="text-2xl font-bold">{profile?.displayName || '社員'}</h1>
</div>
</header>
<main>
{/* 大胆な打刻ボタン */}
<button
onClick={punchIn}
className="w-full bg-emerald-500 text-zinc-900 font-bold text-xl py-6 rounded-2xl shadow-[0_0_30px_rgba(16,185,129,0.3)] active:scale-95 transition-transform"
>
👆 今すぐ打刻
</button>
</main>
</div>
);
}