第289章:認証とミドルウェア
今日は「ログインしてる人だけ見れるAPI」を、ミドルウェアでスパッと守れるようにするよ〜!🥳 (※学習用に、ユーザー確認は“固定ユーザー”で超シンプルにします🙏 本番は後半の「発展」へ✨)
今日のゴール🎯
/api/loginでログイン → JWTを作ってCookieに保存🍪/api/private/*は JWTミドルウェアで保護🛡️- React側から
fetchして「ログインできた!」を体験する💖
まずミドルウェアって何?🧩
ミドルウェアは「ルート処理の前後」に割り込んで、チェックや共通処理をする仕組みだよ✨
await next() したら次へ進んで、途中で Response を返したらそこで止められる(=門番できる!)🚪🧑✈️ (hono.dev)
イメージ図(門番チェーン)🚧

今回の認証方針🍪🪪:JWT + HttpOnly Cookie
- JWT = 「改ざんできない身分証」みたいな文字列🪪
- Cookie(HttpOnly)に入れると、JSから盗みにくくなる(読み取れない)🍪✨
- Edge(Workers)では「サーバーのメモリにセッション保存」みたいなのがやりにくいので、トークン方式が相性いいよ🙆♀️
① まずは秘密鍵(JWT_SECRET)を用意🔑
Cloudflare Workersでは **Secrets(安全な環境変数)**が使えるよ。wrangler secret put で登録できるのが公式の流れ✅ (hono.dev)
ローカル開発用(.dev.vars)
JWT_SECRET="super-secret-change-me"
本番用(CloudflareにSecret登録)
npx wrangler secret put JWT_SECRET
.dev.varsはローカル用で、コミットしないのがお約束だよ🫶 (hono.dev)
② Hono側:ログイン / ログアウト / 保護API を作る🛠️
HonoのJWTミドルウェアは cookie オプションで「Cookieからトークン読む」ができるよ🍪 (hono.dev)
JWTの作成(署名)は sign() を使えるよ✍️ (hono.dev)
Cookie操作は setCookie / deleteCookie が便利〜! (hono.dev)
ファイル名は例だよ!あなたのWorkerエントリ(例:
src/index.ts)に合わせてね😊
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import type { JwtVariables } from 'hono/jwt'
import { sign } from 'hono/jwt'
import { setCookie, deleteCookie } from 'hono/cookie'
type Bindings = {
JWT_SECRET: string
}
type Variables = JwtVariables
const AUTH_COOKIE = 'auth_token' // 本番で強くしたくなったら後で名前を工夫しよ〜🍪
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// ✅ ログイン(学習用:固定ユーザーでOK)
app.post('/api/login', async (c) => {
const { email, password } = await c.req.json<{
email: string
password: string
}>()
// 学習用なので固定で判定(本番はDB + ハッシュ化だよ!)
if (email !== 'test@example.com' || password !== 'password') {
return c.json({ message: 'メールかパスワードが違うよ😢' }, 401)
}
const now = Math.floor(Date.now() / 1000)
const payload = {
sub: 'user_001',
email,
role: 'user',
iat: now,
exp: now + 60 * 60, // 1時間
}
const token = await sign(payload, c.env.JWT_SECRET)
// ローカルは http のことが多いので、https のときだけ Secure を付ける
const isHttps = new URL(c.req.url).protocol === 'https:'
setCookie(c, AUTH_COOKIE, token, {
httpOnly: true,
secure: isHttps,
sameSite: 'Lax',
path: '/',
maxAge: 60 * 60,
})
return c.json({ ok: true })
})
// ✅ ログアウト
app.post('/api/logout', (c) => {
const isHttps = new URL(c.req.url).protocol === 'https:'
deleteCookie(c, AUTH_COOKIE, { path: '/', secure: isHttps })
return c.json({ ok: true })
})
// 🛡️ ここが本題:/api/private/* は全部「認証必須」にする
app.use('/api/private/*', (c, next) => {
// JWTミドルウェアは Cookie からも読める(cookie オプション)
const mw = jwt({
secret: c.env.JWT_SECRET,
cookie: AUTH_COOKIE,
})
return mw(c, next)
})
// ✅ 保護API:ログイン済みなら payload を返す
app.get('/api/private/me', (c) => {
// jwtミドルウェアを通ると jwtPayload が取れるよ
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})
export default app
③ React側:ログインして /me を叩く💻💕
Cookie方式のときは、fetch に credentials: 'include' を付けるのが安心だよ🍪
(特にフロントとAPIが別オリジンのときに重要!)
import { useState } from 'react'
type UserPayload = {
sub: string
email: string
role: string
iat: number
exp: number
}
export default function App() {
const [email, setEmail] = useState('test@example.com')
const [password, setPassword] = useState('password')
const [me, setMe] = useState<UserPayload | null>(null)
const [msg, setMsg] = useState('')
const login = async () => {
setMsg('ログイン中…⏳')
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
setMsg(data.message ?? 'ログイン失敗😢')
return
}
setMsg('ログインOK🎉')
await loadMe()
}
const loadMe = async () => {
const res = await fetch('/api/private/me', { credentials: 'include' })
if (!res.ok) {
setMe(null)
setMsg('まだログインしてないかも🙈')
return
}
const data = await res.json()
setMe(data.user)
setMsg('ユーザー情報GET✨')
}
const logout = async () => {
await fetch('/api/logout', { method: 'POST', credentials: 'include' })
setMe(null)
setMsg('ログアウトしたよ👋')
}
return (
<div style={{ padding: 24, fontFamily: 'system-ui' }}>
<h1>第289章:認証とミドルウェア🔐🧩</h1>
<div style={{ display: 'grid', gap: 12, maxWidth: 420 }}>
<label>
メール📧
<input value={email} onChange={(e) => setEmail(e.target.value)} />
</label>
<label>
パスワード🔑
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={login}>ログイン🚪</button>
<button onClick={loadMe}>/me を読む👀</button>
<button onClick={logout}>ログアウト🧹</button>
</div>
<div>{msg}</div>
{me && (
<pre style={{ background: '#f6f6f6', padding: 12, borderRadius: 8 }}>
{JSON.stringify(me, null, 2)}
</pre>
)}
</div>
</div>
)
}
④ フロントとAPIが別オリジンなら:CORSもセットしよ🌍🔥
HonoにはCORSミドルウェアがあるよ〜!credentials: true と origin 指定がポイント🧁 (hono.dev)
import { cors } from 'hono/cors'
app.use(
'/api/*',
cors({
origin: 'http://localhost:5173',
credentials: true,
})
)
よくある詰まりポイント集😵💫➡️😋
1) Cookieが保存されない🍪💥
Secure: trueのCookieは HTTPSじゃないと保存されないことが多いよ🔒 だから今回みたいに「httpsのときだけSecure」を付けると安全〜🙆♀️SameSite=Noneにしたい場合、Secure必須 & HTTPS必須が基本だよ! (hono.dev)
2) ミドルウェアで取った値を他のリクエストでも使いたい…🥺
c.set() / c.get() は 同じリクエストの間だけ有効だよ(別リクエストに持ち越せない)🧠 (hono.dev)
「永続化したい」なら D1 / KV / Durable Objects の出番だね🌩️🗃️
3) ミドルウェアの順番が大事🧩
登録した順に流れるよ〜!途中で Response を返したらそこで止まる✨ (hono.dev)
発展(本番っぽくするなら)🚀✨
A) “役割”でガード(Authorization)👑
JWTの role を見て「adminだけOK」みたいなミドルウェアを自作すると超それっぽい!😎
(ミドルウェアは c.get('jwtPayload') を使って判定できるよ) (hono.dev)
B) サーバー間APIは Bearer トークンが便利🔑
Webhookやバッチみたいな「ブラウザじゃない通信」なら Authorization: Bearer ... が扱いやすいよ〜! (Cloudflare Docs)
C) 外部認証(Auth0/Clerk等)と繋ぐなら JWK ミドルウェア🪪🌐
公開鍵(JWKS)でトークン検証してくれるミドルウェアがあるよ!Cookieから取り出す設定もできる✨ (hono.dev)
ミニ課題📝(10〜20分でできるよ🎀)
/api/private/adminを追加してみよう👑role === 'admin'のときだけ200 OK、それ以外は403にしてみよう🚫- ログイン時のpayloadの
roleをadminに変えたら通る!…みたいにテスト✨
必要なら、次の「第290章:最終課題」に繋がる形で、
- D1にユーザーを保存してパスワードをハッシュ化🔐
- Turnstile(bot対策)🤖🚫
- CSRF対策🛡️ まで含めた“ほぼ実戦セット”版も作れるよ〜!