LINEミニアプリ(LIFF)からスマートホームを操作したい、外部から自宅サーバーのAPIを安全に叩きたい——そんな用途には、PythonのWebフレームワーク「Flask」で自作APIサーバーを構築し、NginxのリバースプロキシでHTTPS公開するアーキテクチャが最適です。
本記事では、FlaskのBlueprint機能を使って外部向けAPI・内部向けAPI・Webhookを整理し、LINE IDトークン認証とレートリミットを組み込んで外部公開する実装手順を解説します。systemdによるプロセス管理も設定し、サーバー再起動時に自動起動する構成まで仕上げます。
なぜこの機能を作ったのか?
LINEミニアプリ(LIFF)でGoogle Homeに発話させたり、外出先からスマートホームの状態を確認したりするには、インターネット経由で呼び出せるAPIエンドポイントが必要です。
しかし「APIを外部公開する=誰でもアクセスできる」では困ります。家族のLINEアカウントからの呼び出しだけを許可し、それ以外はすべて弾く仕組みが必要です。
LINE IDトークン(JWT)を使った認証を自前で実装することで、LINEアカウントを持つ特定ユーザーだけが使えるプライベートAPIを無料・自前で構築できます。NginxがHTTPS暗号化とパスルーティングを担当し、FlaskはビジネスロジックとAPI認証に専念するシンプルな役割分担です。

外出先からLINEミニアプリでGoogle Homeに喋らせたり、スマートホームを操作できるAPIを作りたい!
実現したいこと
- LINEミニアプリ(LIFF)から
https://xxxx.tplinkdns.com/external/api/...でAPIを呼び出せる - LINE IDトークン認証で、許可した家族のLINEアカウントからのリクエストだけを受け付ける
- 外部向けAPI・内部向けAPI・WebhookをFlaskのBlueprintで分離管理する
- Flask-Limiterでレートリミットを設け、過剰なリクエストを防ぐ
- systemdサービスで自動起動・プロセス監視を行う
- NginxリバースプロキシでHTTPS暗号化しながらFlaskに転送する
この記事でわかること
- FlaskのBlueprint機能でAPIエンドポイントを役割ごとに分離する方法
- LINE IDトークン(JWT)をFlaskで検証する認証デコレーターの実装方法
- Flask-LimiterでAPIのレートリミットを設定する方法
- systemdサービスファイルでFlaskアプリを自動起動・再起動させる方法
- Nginxの
proxy_passでURLパスを変換しながらFlaskに転送する方法
必要な準備と用意するもの
ハードウェア
- 自宅サーバー(本記事では MINISFORUM NUCBox G3 Plus / Ubuntu Server 24.04)
ソフトウェア/サービス
- Nginx(インストール済み・稼働中)
- 参考記事:「NginxでHTTPSリバースプロキシを構築する」
- Let’s Encrypt SSL証明書(取得済み)
- Python 3.12 + venv(仮想環境)
- Flask 3.1.2
- Flask-Limiter 4.1.1
- LINEアカウント + LINE Developersのチャンネル(LIFFアプリ)
- LINE Channel ID が必要
完成イメージ
設定完了後のAPIエンドポイントと利用方法のイメージです。
| 外部URL(Nginx) | Flaskの内部URL | 認証 | 用途 |
|---|---|---|---|
/external/api/ping | /api/ping | なし | 疎通確認 |
/external/api/check_user | /api/check_user | LINE IDトークン | 認証テスト |
/external/api/speak_googlehome | /api/speak_googlehome | LINE IDトークン | Google Home発話 |
/webhooks/... | /webhooks/... | 各サービス固有 | 外部Webhook受信 |
/internal/... | /internal/... | LAN内IPのみ許可 | LAN内専用API |
NginxのURL(/external/)とFlask内部のBlueprintパス(/api/)は意図的に異なる名前にしています。Nginxのパス名を変えるだけで外部公開URLを変更でき、Flaskのコードは変更不要になります。
システムの仕組み
リクエストの流れ
外部からAPIを呼び出したとき、リクエストは以下の経路で処理されます。
LINEミニアプリ(ブラウザ) → https://xxxx.tplinkdns.com/external/api/speak_googlehome
↓ Nginx(443番ポート、SSL終端)
↓ proxy_pass http://127.0.0.1:8000/api/(/external/ → /api/ にパス変換)
↓ Flask(8000番ポート)
↓ Blueprint「api」→ dispatcher → LINE IDトークン検証 → エンドポイント関数実行
↓ Google Home に発話リクエスト送信
ファイル・ディレクトリ構成
smarthome/
└── interfaces/
└── inbound/
├── http_server.py # Flaskアプリ本体(エントリーポイント)
├── api/ # 外部向けAPI Blueprint
│ ├── __init__.py # Blueprint定義
│ ├── dispatcher.py # 認証デコレーター・APIエンドポイント
│ └── routes.py # シンプルなルート(pingなど)
├── internal/ # LAN内向けAPI Blueprint
│ ├── __init__.py
│ └── dispatcher.py
└── webhooks/ # Webhook受信 Blueprint
├── __init__.py
└── routes.py
LINE IDトークン認証の仕組み
LINEミニアプリはユーザーがログインすると LINE IDトークン(JWT)を発行します。このトークンをAPIリクエストの Authorization: Bearer <token> ヘッダに乗せて送信し、FlaskはLINEのAPIエンドポイント(https://api.line.me/oauth2/v2.1/verify)で検証します。
| ステップ | 処理内容 |
|---|---|
| ① LINEアプリでログイン | LINEがIDトークン(JWT)を発行 |
| ② APIリクエスト送信 | Authorization: Bearer <IDトークン> ヘッダ付きでリクエスト |
| ③ Flask受信 | デコレーター @require_line_auth がトークンを抽出 |
| ④ LINE APIで検証 | LINEの検証APIにトークンを送って正当性を確認 |
| ⑤ ユーザー確認 | LINEのユーザーID(sub)が許可リストにあるか確認 |
| ⑥ 処理実行 or 拒否 | 許可ユーザーならエンドポイント関数へ。それ以外は401/403を返す |
実装のポイント
BlueprintでAPIの役割を明確に分離する
Flaskの Blueprint 機能を使うと、外部向けAPI・LAN内向けAPI・Webhookを別ファイルに分離できます。それぞれに個別の url_prefix を設定でき、コードが整理されてメンテナンスしやすくなります。
IDトークン検証結果をキャッシュしてLINE APIの呼び出し回数を削減する
LINE IDトークンの有効期限(exp)まではキャッシュに保持し、同じトークンで繰り返しリクエストが来ても外部API呼び出しを1回に抑えます。これによりレスポンスの高速化と外部API依存度の低減を実現しています。
Nginxのパス変換でFlask内部のURL設計を自由にできる
Nginxの location /external/ に proxy_pass http://127.0.0.1:8000/api/; と設定することで、外部URL(/external/)とFlask内部のパス(/api/)を別々に管理できます。外部公開URLを変えたくなってもFlaskのコードは一切変更不要です。
Flask組み込みサーバーは本番環境に使わない
app.run() で起動するFlask組み込みサーバーはシングルスレッドで、本番環境には非推奨です。本記事の構成ではNginxがリバースプロキシとしてリクエストを受け取り、Flaskへの同時リクエストは実際にはNginxがバッファリングします。さらに並列処理が必要な場合はGunicornやuvicornの導入を検討してください。
AuthorizationヘッダーをNginxで明示的に転送する
Nginxのデフォルト設定では Authorization ヘッダーが下流に転送されない場合があります。proxy_set_header Authorization $http_authorization; を明示的に追加することで、LINE IDトークンをFlaskが受け取れるようになります。
事前準備
① Python仮想環境のセットアップ
システム全体のPythonに影響を与えないよう、仮想環境(venv)を使います。
# プロジェクトフォルダに移動
cd ~/Projects
# 仮想環境を作成
python3 -m venv venv
# 仮想環境を有効化
source venv/bin/activate
② Flaskと必要なパッケージのインストール
pip install flask flask-limiter requests
インストール後にバージョンを確認します。
pip show flask flask-limiter
# Name: Flask
# Version: 3.1.2
# Name: Flask-Limiter
# Version: 4.1.1
③ LINE DevelopersでChannel IDを確認する
LINE IDトークンの検証には LINE Channel ID が必要です。LINE Developersコンソール(https://developers.line.biz/)でLIFFを設定したチャンネルの「Basic settings」ページにある「Channel ID」を控えておきます。
実装方法
① Blueprint定義(api/__init__.py)
まず外部向けAPIのBlueprintを定義します。
from flask import Blueprint
# 外部向けAPIをまとめるBlueprint
api_bp = Blueprint(
"api", # Blueprint識別名(アプリ内で一意)
__name__, # このBlueprintが属するモジュール名
url_prefix="/api"
)
# dispatcherとroutesを読み込むことでエンドポイントをBlueprintに登録する
from . import dispatcher
from . import routes
__all__ = ["api_bp"]
② 認証デコレーターとAPIエンドポイント(api/dispatcher.py)
LINE IDトークン認証の核心部分です。認証デコレーター @require_line_auth を作成し、保護したいエンドポイントに付与します。
from functools import wraps
from flask import g, request, abort, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import time
import requests
from . import api_bp
# ----------------------------------------
# Flask-Limiter の初期設定
# ----------------------------------------
limiter = Limiter(
key_func=get_remote_address,
default_limits=[] # デフォルトは制限なし(エンドポイントごとに個別設定)
)
# ----------------------------------------
# 設定
# ----------------------------------------
LINE_CHANNEL_ID = "xxxxxxxxxx" # LINE DevelopersのChannel ID
# 許可するLINEユーザーのIDリスト(各ユーザーのsub値)
LINE_USER_MAP = {
"Uxxxxxxxxxxxxxxxxxxxx": {"name": "user1"},
"Uyyyyyyyyyyyyyyyyyyyyyy": {"name": "user2"},
}
# ----------------------------------------
# LINE IDトークン検証(LINE APIに問い合わせ)
# ----------------------------------------
def verify_line_id_token(id_token):
url = "https://api.line.me/oauth2/v2.1/verify"
data = {
"id_token": id_token,
"client_id": LINE_CHANNEL_ID
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
r = requests.post(url, data=data, headers=headers, timeout=3)
if r.status_code != 200:
return None
return r.json()
# ----------------------------------------
# IDトークン検証キャッシュ(有効期限内は再検証しない)
# ----------------------------------------
token_cache = {}
def verify_line_id_token_cached(id_token):
now = time.time()
# 期限切れキャッシュの削除
for token in list(token_cache.keys()):
if token_cache[token]["exp"] <= now:
del token_cache[token]
# キャッシュヒットなら返す
if id_token in token_cache:
cached = token_cache[id_token]
if cached["exp"] > now:
return cached["data"]
# キャッシュミス:LINE APIで検証してキャッシュに保存
user_info = verify_line_id_token(id_token)
if user_info:
token_cache[id_token] = {
"data": user_info,
"exp": user_info.get("exp", 0)
}
return user_info
# ----------------------------------------
# 認証デコレーター
# ----------------------------------------
def require_line_auth(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify({"status": "error", "reason": "missing token"}), 401
id_token = auth.replace("Bearer ", "")
user_info = verify_line_id_token_cached(id_token)
if not user_info:
return jsonify({"status": "error", "reason": "invalid token"}), 401
user_id = user_info.get("sub")
if user_id not in LINE_USER_MAP:
return jsonify({"status": "error", "reason": "forbidden"}), 403
# エンドポイント関数内で g.user からユーザー情報を参照できる
g.user = {
"line": user_info,
"config": LINE_USER_MAP[user_id]
}
return f(*args, **kwargs)
return wrapper
# ----------------------------------------
# APIエンドポイント
# ----------------------------------------
@api_bp.route('/api/check_user', methods=['GET'])
@require_line_auth
def check_user():
# 認証テスト用エンドポイント
return jsonify({"status": "ok"}), 200
@api_bp.route('/api/speak_googlehome', methods=['POST'])
@require_line_auth
@limiter.limit("10 per minute") # 1分間に10回まで
def speak_googlehome():
# Google Homeに指定メッセージを発話させる
try:
data = request.get_json()
message = data.get("message")
if not message or len(message) > 255:
return jsonify({"status": "error", "reason": "invalid message"}), 400
# Google Homeへの発話処理(別モジュールで実装)
# google_notify.speak(message)
return jsonify({"status": "ok"}), 200
except Exception:
return jsonify({"status": "error"}), 500
③ Flaskアプリ本体(http_server.py)
各Blueprintをまとめて登録するエントリーポイントです。
from flask import Flask
from interfaces.inbound.api import api_bp
from interfaces.inbound.api.dispatcher import limiter
from interfaces.inbound.internal import internal_bp
from interfaces.inbound.webhooks import webhooks_bp
app = Flask(__name__)
limiter.init_app(app) # Flask-LimiterをFlaskアプリに紐付ける
# Blueprintを登録(url_prefixで各Blueprintのベースパスを設定)
app.register_blueprint(api_bp, url_prefix="/api")
app.register_blueprint(internal_bp, url_prefix="/internal")
app.register_blueprint(webhooks_bp, url_prefix="/webhooks")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
④ systemdサービスの設定
Flaskアプリをサーバー起動時に自動起動させるsystemdサービスファイルを作成します。
sudo nano /etc/systemd/system/smh-http-server.service
[Unit]
Description=Smarthome HTTP Server (Flask)
After=nginx.service
Wants=nginx.service
[Service]
# 仮想環境のPythonでhttp_server.pyを起動する
ExecStart=/home/user/Projects/venv/bin/python /home/user/Projects/smarthome/interfaces/inbound/http_server.py
Restart=always # 異常終了時に自動再起動
RestartSec=60 # 再起動までの待機時間(秒)
Type=simple
User=user # 実行ユーザー(rootは使わない)
[Install]
WantedBy=multi-user.target
サービスを有効化して起動します。
# systemdにサービスファイルを認識させる
sudo systemctl daemon-reload
# OS起動時に自動起動する設定 & 今すぐ起動
sudo systemctl enable --now smh-http-server
# 起動状態を確認
sudo systemctl status smh-http-server
⑤ Nginx設定にAPIのロケーションブロックを追加する
既存のNginx HTTPS設定(443番ポートのサーバーブロック)に、外部向けAPIとWebhookのロケーションブロックを追加します。
server {
listen 443 ssl;
server_name xxxx.tplinkdns.com;
# SSL設定(省略)...
# -----------------------------------------
# /external/ : 外部向けFlask API
# -----------------------------------------
location /external/ {
proxy_pass http://127.0.0.1:8000/api/; # /external/ → /api/ にパス変換
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
# LINEのBearerトークンをFlaskに転送する(これがないと認証が通らない)
proxy_set_header Authorization $http_authorization;
# アクセスログを外部API用ファイルに分離
access_log /srv/nginx/log/api.log detailed;
}
# -----------------------------------------
# /webhooks/ : 外部Webhook受信
# -----------------------------------------
location /webhooks/ {
proxy_pass http://127.0.0.1:8000/webhooks/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
}
}
LAN内向けサーバーブロック(80番ポート)にはIPアドレス制限付きの内部APIを追加します。
server {
listen 80;
server_name 192.168.68.89;
# -----------------------------------------
# /internal/ : LAN内専用API(IPアドレス制限あり)
# -----------------------------------------
location /internal/ {
proxy_pass http://127.0.0.1:8000/internal/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
allow 192.168.68.0/24; # LAN内のみ許可
deny all;
access_log /srv/nginx/log/api_internal.log detailed;
}
}
⑥ Nginx設定の確認とリロード
# 構文チェック
sudo nginx -t
# 問題なければリロード
sudo systemctl reload nginx
コードの解説
Blueprint の url_prefix と Nginx の proxy_pass の関係
FlaskのBlueprintは url_prefix="/api" で登録されているため、エンドポイント @api_bp.route('/api/speak_googlehome') の最終URLは /api/api/speak_googlehome になります。
url_prefix の二重定義に注意
http_server.py で app.register_blueprint(api_bp, url_prefix="/api") と、__init__.py の Blueprint(..., url_prefix="/api") の両方でプレフィックスを定義すると二重になります。どちらか一方にまとめるか、__init__.py 側は url_prefix を省略して register_blueprint 側だけで指定するのがシンプルです。
Nginx の proxy_pass パス変換の詳細
| ブラウザが送るURL | Nginx設定 | Flaskが受け取るURL |
|---|---|---|
/external/api/ping | location /external/ { proxy_pass http://127.0.0.1:8000/api/; } | /api/ping |
/webhooks/ping | location /webhooks/ { proxy_pass http://127.0.0.1:8000/webhooks/; } | /webhooks/ping |
外部向けAPIは /external/ → /api/ とパスが変換されます。Webhookは /webhooks/ のまま変換されません。このようにNginxで外部URLとFlask内部URLを分離することで、Flask側のコードを変えずに外部公開パスを柔軟に変更できます。
Flask-Limiter によるレートリミット
@api_bp.route('/api/speak_googlehome', methods=['POST'])
@require_line_auth
@limiter.limit("10 per minute") # 同じIPから1分間に10回まで
def speak_googlehome():
...
@limiter.limit("10 per minute") を付けると、同一IPアドレスからのリクエストを1分間に10回に制限します。制限を超えると自動的に 429 Too Many Requests を返します。Google Home発話APIのように外部に副作用のある処理に設定しておくと、誤操作や悪用によるスパムを防げます。
動作確認
① Flaskサービスの起動確認
sudo systemctl status smh-http-server
# Active: active (running) と表示されればOK
# ポート8000でListenしているか確認
ss -tlnp | grep 8000
# LISTEN 0 128 0.0.0.0:8000 ...
② pingエンドポイントで疎通確認
# サーバー内からFlaskに直接アクセス(認証なし)
curl http://127.0.0.1:8000/api/ping
# {"status": "ok"}
# Nginx経由でアクセス(外部URL)
curl https://xxxx.tplinkdns.com/external/api/ping
# {"status": "ok"}
③ LINE IDトークン認証の確認
# 認証なしでアクセス → 401 Unauthorized
curl https://xxxx.tplinkdns.com/external/api/check_user
# {"reason":"missing token","status":"error"}
# 有効なLINE IDトークンを付けてアクセス → 200 OK
curl -H "Authorization: Bearer <LINEのIDトークン>" \
https://xxxx.tplinkdns.com/external/api/check_user
# {"status":"ok"}
④ トラブルシューティング
Nginx経由でアクセスすると401になる(直接8000番では動く)
Authorization ヘッダーがNginxで転送されていない可能性があります。Nginx設定に proxy_set_header Authorization $http_authorization; が追加されているか確認します。
502 Bad Gateway が返る
Flaskが起動していません。sudo systemctl status smh-http-server で状態を確認し、エラーログは journalctl -u smh-http-server -n 50 で確認します。
LINE IDトークン検証が常に失敗する
LINE_CHANNEL_ID が正しいか確認します。LIFFアプリを作成したチャンネルのChannel IDと一致している必要があります。また、トークンの有効期限(通常数分〜数時間)が切れていないか確認します。
レートリミットエラー(429 Too Many Requests)が頻発する
Nginxの proxy_set_header X-Real-IP $remote_addr; が設定されていることを確認します。これがないとFlask-LimiterがNginxのループバックIP(127.0.0.1)をクライアントIPと誤認し、全リクエストが同一IPとしてカウントされます。
まとめ
本記事では、FlaskのBlueprint構成でスマートホーム向けAPIを構築し、Nginxリバースプロキシ経由でHTTPS外部公開する方法を解説しました。
- FlaskのBlueprintで外部API・内部API・Webhookを役割ごとに分離した
- LINE IDトークンをLINE APIで検証する認証デコレーター
@require_line_authを実装し、特定LINEユーザーのみに限定した - 検証結果をキャッシュして外部API呼び出し回数を削減した
- Flask-Limiterでエンドポイントごとにレートリミットを設定した
- systemdサービスでサーバー再起動時の自動起動・異常終了時の自動再起動を設定した
- Nginxの
proxy_passでURLパスを変換し、外部公開パスとFlask内部パスを分離した proxy_set_header Authorization $http_authorization;でBearerトークンをFlaskに転送した
このAPIサーバーを基盤として、Google Home発話・SwitchBot操作・センサーデータ取得など、スマートホームのさまざまな機能をLINEミニアプリから操作できるようになります。

