LINE画像送信APIの代替手段を検討して実装した話
教室管理システムのシフト表画像をLINEで自動送信する機能の紹介記事です
はじめに
この記事では、教室管理システムに実装した、LINEで講師シフト表を安全に共有する仕組みを紹介します。このシステムの概要についてはProjectの教室管理システムをご覧ください。
なぜLINEで画像送信したいか
私のバイト先ではLINEグループ上でシフト表を画像で管理者が送信する形で長年運用していました。そのため、閲覧するために外部サービスを使うことはなるべく変えずにシステムを運用したかったんですよね。
しかし、機能を開発していくと、今回の用途ではかなり致命的な仕様に気づいてしまいました…
画像メッセージでは、LINEプラットフォームが取得できるHTTPSの画像URLを指定する必要がある
xe-pc23
えぇ・・・まじかよ..
期限付きURLを使うこともできるけど、誰でも見れてしまう。
このAPIを使うならバイト先のシフト表を世に公開してしまう..
LINEのAPIの仕様
ここでLINEの画像を送信するAPIの仕様を見てみましょう。
developers.line.biz
Messaging APIリファレンス
https://developers.line.biz/ja/reference/messaging-api/#image-message
LINEのMessaging APIリファレンスによると画像メッセージを送信するには以下のようなJSON形式にしないといけないようです。
{
"type": "image",
"originalContentUrl": "https://example.com/original.jpg",
"previewImageUrl": "https://example.com/preview.jpg"
}
originalContentUrlは画像のそのもので、previewImageUrlはLINEで画像送ったときにトークに表示されるやつですね。
このURLを知っていれば誰でもその画像をみれてしまいますね… URLを入手した人をシステム側で講師ごとに認証・認可することもできないし。
xe-pc23
困った困った。
代替手段を考える
当然バイト先のシフト表をオープンに公開するなんてことはできませんよね。代替手段をいくつか考えてみました。
①GUIでのシフト表画像送信を自動化するプログラムを組む
APIを使わずに公式アプリをGUIベースで自動操作するプログラムを組むとできるかもしれませんが、LINEヤフー共通利用規約で禁止されている”BOTなどの技術的手段による不正な操作”に該当する恐れがあって、グレーなので自動操作構成は採用しませんでした。
この辺はどうなんでしょうかね。許可されてたらAPIメッセージ送信上限で料金変わる意味無くなるしやっぱりアウトなんでしょうか。わからん。
terms2.line.me LINEヤフー共通利用規約 https://terms2.line.me/line_terms?lang=ja②Google Driveなどに画像をおいてブラウザでシフト画像を見せる
これなら規約的には問題ないですね。しかし、今までLINEでチャットの画像を開けるだけだったのがブラウザを起動してから見せる方法は表示までが遅いですよね。さらにGoogleアカウントで権限設定するのもあって、LINE以外の要素が加わるのはスマートじゃないですよね?
③LINEでの連絡をやめて専用アプリを作る
これはめんどくさい。リリースするまで時間がかかりすぎる。
検討した結果以下のような結論に辿り着きました。
LIFFを使用してシフト表を送信する
LINEにはLINE Front-end Framework(LIFF)というWEBアプリプラットフォームがあります。
これを使うと、LINE上で開いたWebアプリからLINEログイン状態を確認し、認証済みのLINEアカウントを識別するためのIDトークンを取得できます。 (このシステムではIDのみを利用しています。)
xe-pc23
これを使うと外部サービス使わずに認証もできて、表示もLINE上で完結できるやん!
今回はこの技術を採用して、シフト表画像を送信表示するシステムを開発しました。
全体的な技術内容
アーキテクチャ
全体のアーキテクチャとしてはざっくり以下の通りです。

そのため今回採用したこの仕組みでは、シフト表PNGを画像メッセージとしてLINEに直接送ってるわけではないです。
セキュリティ面
教室管理システムのAPIは、Cloudflare Accessを利用して外部からのアクセスを制限しています。
一方、LINE連携用の公開エンドポイントは、通常のCloudflare Access認証を前提にできません。
LINE WebhookはLINEプラットフォームから直接送られ、LIFF用APIは講師のLINE内ブラウザ、または外部ブラウザから呼び出されます。そのため、必要なパスだけを別のAccess Applicationとして切り出し、Bypass Policyを設定しています。
BypassされたエンドポイントではCloudflare Accessによるユーザー認証を行わないため、その代わりにLINE Webhookの署名検証やIDトークン検証、システム側の認可処理を行っています。
システム上の講師アカウントとLINEアカウントの連携
これはすでにシステム内に存在するアカウントに対して、本人がLINEで認証したLINEアカウントを紐付けて、管理者の承認後にだけシフト表の閲覧や勤務前通知(この記事では関係ないけど)を使えるようにする仕組みです。
これがないとLINE連携したシステムは何も動かないです。
全体的な流れは以下の通りです。
flowchart LR
A["管理者が連携URL発行"]
--> B["講師がLIFFを開く"]
--> C["LINEログイン・同意"]
--> D["IDトークン検証"]
--> E["講師とLINEを仮連携"]
--> F["管理者が承認"]
--> G["連携完了"]
また発行するURLは次のような形です。
https://liff.line.me/<LIFF_ID>/link/<一回限りのトークン>
<一回限りのトークン> は、ランダムに生成される秘密の値です。サーバーにはトークンそのものではなく、ハッシュ値だけを保存してます。
このトークンには以下の制約を持たせました。
- 有効期限は30分
- 一度使ったら再利用不可
- 新しいURLを発行すると、同じ講師向けの未使用URLは無効化
- どの講師アカウントのために発行されたかをサーバー側で保持
仕組みの解説(工夫したところ)
LIFFでLINEアカウントを確認する仕組み
講師が連携URLを開くと、通常の管理画面ではなく、LINE連携専用の画面が表示されます。
この画面ではLIFFを初期化し、LINEにログインしているかを確認します。外部ブラウザで開いた場合も、必要に応じてLINEログイン画面へ遷移します。
LINEログイン後、フロントエンドはLINEからIDトークンを受け取ります。
IDトークンはこの操作をしているLINEユーザーが誰なのかをLINEが証明するための情報で、LINE内部のユーザー識別子を取得するために使ってます。
講師には連携前に、以下の画像のように同意を求めます。
サーバー側で本物のLINE認証かを確認する仕組み
講師が「同意して連携する」を押すと、フロントエンドは次の情報をサーバーへ送ります。
- 連携URLに含まれていた一回限りのトークン
- LINEから取得したIDトークン
- 同意したこと
それからサーバ側はIDトークンをそのまま信用しない設計にしています。
というのも、ブラウザから送られる値は利用者が書き換えられる可能性があるためです。検証せずに受け入れると、攻撃者が任意に作った値や、別のアプリ向け・有効期限切れのトークンを送信し、他人のLINEアカウントとして連携を申請できてしまいます。
その結果、本来とは異なる講師アカウントとLINEアカウントが紐付き、勤務前通知が誤った相手に届いたり、シフト表の閲覧権限が意図しない人に与えられたりするおそれがあります。
それを防ぐためにLINE公式のトークン検証APIへ問い合わせ、次のような内容を確認します。
- 発行者がLINEであること
- このシステム用に設定したLINE Loginチャネル向けのトークンであること
- LINEユーザー識別子が存在すること
- 有効期限が切れていないこと
また一回限りの連携URLを、同時アクセスでも一度しか使えないようにする工夫もしています!
IDトークンの検証後、サーバーは連携URLに含まれるトークンをハッシュ化し、発行済みの連携トークンと照合します。
このとき、事前に未使用かつ有効期限内のトークンかを確認するだけでは不十分なんですよね。講師が連打した場合や、同じURLを複数の端末でほぼ同時に開いた場合、複数のリクエストが同時に「まだ未使用」と判断してしまう可能性があるためです。
そこで今回の実装では、使用可否の確認と使用済みへの更新を、次のような条件付きのUPDATEで行うようにしています。
UPDATE line_link_tokens
SET used_at = ?
WHERE token_hash = ?
AND used_at IS NULL
AND expires_at > ?
この更新で変更された行数がちょうど1件だった場合だけ、トークンを取得できたものとして後続のLINEアカウント連携処理へ進みます。
リクエストA・Bが同時に届く
↓
Aが条件付きUPDATEに成功し、used_atを記録する
↓
BのUPDATE実行時には、すでにused_atが入っている
↓
Bは更新対象が0件となり、連携を続行できない
事前に対象講師アカウントを特定する処理はありますが、「一回だけ使える」という保証は、この条件付きUPDATEの結果で判断しています。
また、連携情報の保存中にエラーが起きた場合でも、いったん使用済みにしたトークンを未使用へ戻さない仕組みにしています。連携URLの再利用よりも安全性を優先し、必要なら管理者が新しい連携URLを発行する設計にしています。
xe-pc23
再発行もそんなに手間かからないし安全性大事 -_-b
LINEユーザIDの管理の仕組み
連携が成立すると、システムの講師アカウントとLINEアカウントの対応を保存します。
システムのユーザーアカウント
↓
LINE連携情報
↓
暗号化されたLINEユーザーID
LINEユーザーIDは、通知の送信先や閲覧権限の確認に必要です。そのため保存はしますが、平文のままデータベースには保存しません。
- 元のLINEユーザーIDはAES-GCMで暗号化して保存
- 検索用には、暗号化用の元秘密鍵に用途識別子を加えてSHA-256で導出した別の鍵を用い、LINEユーザーIDに対するHMAC-SHA-256値を保存
- 表示名やプロフィール画像は保存しない
- 1つのLINEアカウントを複数のシステムアカウントへ重複連携できないようにしている
毎回LINEユーザーIDを復号して照合せずに、検索時にハッシュを使うようにしています。
連携直後は管理者の承認を待つ仕組み
連携が完了しても、すぐには利用できません。
連携情報はまず pending、つまり承認待ちの状態で保存されます。管理者が講師一覧から承認すると active になり、初めてシフト表の閲覧と個別通知が有効になります。
連携申請
↓
pending(承認待ち)
↓ 管理者が承認
active(利用可能)
この承認ステップを入れることで、誤ったLINEアカウントを連携してしまった場合や、本人確認が必要な場合にも、管理者が利用開始前に確認できます。
また、管理者が連携を解除すると状態は disabled になり、シフト表の閲覧と個別通知は停止します。
シフト表のURLだけでは画像を見られない仕組み
月間シフト表は、このシステム作る前から使っていたバイト先のグループラインがあるので、そこに送信するようにしています。(グループID使って)
送信されるURLは以下の通りです。
https://liff.line.me/<LIFF_ID>/view/<documentId>
ここで使われる documentId は、どのシフト表を開くかを示すIDです。パスワードや閲覧権限そのものではありません。
画像を取得する際、サーバーは毎回以下を確認します。
- LIFFで取得したLINE IDトークンが有効か
- そのLINEアカウントが連携済みか
- 管理者承認済みで
activeか - 対応するシステム講師アカウントが有効か
- そのシフト表が最新の内容か
すべて満たした場合だけ、非公開ストレージにある画像を返します。
そのため、LINEグループ内で閲覧URLが共有されても、未連携の人や未承認の人は画像を取得できないようにしています。 実際の画面とかはProjectの教室管理システムの写真を見てください。
シフト更新時は古い画像を自動で無効化する仕組み
さっきの章にあった5.のシフト表が最新の内容かというところです。
シフト表には、作成時点のシフト内容から計算したハッシュ値を保存しています。
画像そのものから作るのではなく、画像の元になる月間シフト・授業情報を一定の順番で並べ、JSON文字列にしたうえでSHA-256を計算しています。
const snapshot = {
shifts: [
{
id,
teacher_id,
classroom_id,
date,
start_time,
end_time,
status,
is_key_opener,
updated_at,
},
// 対象月のシフトを日時順に並べる
],
lessons: [
{
id,
classroom_id,
date,
start_time,
end_time,
status,
},
// 対象月の授業を日時順に並べる
],
blockers: [
// 送信を止めるべき要確認項目
],
}
const sourceHash = SHA-256(JSON.stringify(snapshot))
たとえば、9月のシフト表を公開した後に、ある講師の勤務時間を 13:00〜17:00 から 14:00〜17:00 に変更したとします。
公開時のシフト内容
↓
sourceHash: A
勤務時間を変更
↓
現在のシフト内容
↓
sourceHash: B
変更前と変更後では元データが異なるため、A と B は一致しません。
画像を閲覧する際、サーバーは画像を作成した時のハッシュ値と現在のシフト内容から計算したハッシュ値を比較します。
一致する
→ 画像は最新と判断し、表示する
一致しない
→ 画像は古いと判断し、表示しない
そのため古い閲覧URLを開くと、公開時のシフト内容と現在のシフト内容のハッシュ値が一致しないため、サーバーは画像を返しません。
念の為、PNGデータにもハッシュ持たせてR2上の完全性を確認できるようにしてます。
まとめ
LINEの画像メッセージは、画像URLを指定して送信する仕組みです。そのまま講師シフト表を送ると、URLを知っている人が期限内に画像を取得できてしまうため、講師ごとの連携状態や管理者承認を反映できません。
そこでこのシステムでは、シフト表画像そのものではなく、LIFFを開くFlex Messageの閲覧ボタンをLINEグループへ送る形にしました。
LIFFでLINEアカウントを確認し、サーバー側で次の条件を毎回確認することで、シフト表を安全に共有しています。
- LINEのIDトークンが正しいこと
- システム上の講師アカウントとLINEアカウントが連携済みであること
- 管理者が連携を承認していること
- 対象講師アカウントが有効であること
- シフト表が最新の内容であること
また、連携URLは一回限りにし、LINEユーザーIDは暗号化して保存しています。既存のLINEグループ運用を大きく変えずに、シフト表の閲覧権限をシステム側で制御できるようにしました。
個別の勤務前通知については、別の記事で仕組みを紹介する予定です。
xe-pc23
読んでくださりありがとうございました!!