はじめに
席替えアプリでログイン機能をつけると、ログインをしているかどうかを各ページで検証することが必要になりました。しかし、各ページに毎回if文を書くことは冗長であり、まとめてProxyにその役割を任せることがコードの見通しが良くなり、管理がしやすくなるとアプリ作成を通して感じました。
今回は、Proxyを使った認証のルーティングについてまとめていきます。
Proxyとは
以下、公式のProxyからの引用です。
プロキシは、ルートがレンダリングされる前に実行されます。認証、ログ記録、リダイレクト処理など、サーバー側のカスタムロジックを実装する場合に特に役立ちます。
レンダリングされる前に実行されるので、クライアントと、サーバーの間、サーバーの上にいる門番のイメージです。
(例)クライアントサイドでリンクをクリックしたら、このProxy(門番)が入っていいかどうかチェックしてくれる
Proxy作成にあたって
2段階の手順を踏んで、席替えアプリでは実装しました。
プロジェクト全体のProxyの作成
import { NextRequest } from 'next/server';
import { updateSession } from './lib/supabase/proxy.auth';
// リクエストが完了する前にプロキシを作成し、サーバー上でコードを実行
export const proxy = async (request: NextRequest) => {
return await updateSession(request);
};
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|update-history|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
updateSessionについては後述します。
ここでは、認証のルーティングを設定したいページを指定するmatcherについて見ていきます。
https://nextjs.org/docs/app/api-reference/file-conventions/proxy#matcher
ログインをしていたら入ってほしくないページなどをここに記述していきます。ただし、今回の書き方は、特定のパスを除外する書き方をしています。(ここに書いていれば、proxyは無視してくれる)
上記のコードであれば、画像やupdate-historyのページということです。
認証担当のProxyの作成
import { createServerClient } from '@supabase/ssr';
import { NextRequest, NextResponse } from 'next/server';
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll(); // リクエストからcookieを読んで、Supabaseに渡す
},
setAll(cookiesToSet) {
// リクエストオブジェクトにも反映
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
// レスポンスを再生成(新しいCookieを含めるため)
supabaseResponse = NextResponse.next({ request });
// レスポンスのCookieに書き込む(ブラウザに返される)
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options),
);
},
},
},
);
const { data } = await supabase.auth.getClaims();
const claims = data?.claims ?? null;
const isFullUser = claims?.is_anonymous === false;
const isAnonymous = claims?.is_anonymous === true;
const isGuest = claims === null;
const { pathname } = request.nextUrl;
// ユーザーが存在しないかつ/classroom配下もしくは/user/updateへのアクセスがあったとき
if (isGuest && (pathname.startsWith('/classroom') || pathname.startsWith('/user/update'))) {
return NextResponse.redirect(new URL('/user/signin', request.url));
}
// 仮ログインしているかつ/または/user/signin配下へのアクセスがあったとき
if (isAnonymous && (pathname === '/' || pathname.startsWith('/user/signin'))) {
return NextResponse.redirect(new URL('/classroom', request.url));
}
// ログインしているかつ/または/user配下へのアクセスがあったとき
if (isFullUser && (pathname === '/' || pathname.startsWith('/user'))) {
return NextResponse.redirect(new URL('/classroom', request.url));
}
return supabaseResponse;
}
ここが前述したupdateSessionになります。
長いので前後半に分けます。
【前半部分】 getAll()とsetAll()について
そもそもの話になりますが、
SupabaseはセッションをCookieで管理しており、Proxyはサーバーでのみ動くので、クライアントサイドのブラウザの中にあるCookieには直接アクセスできません。
そこで、getAll()とsetAll()を使い、クライアントサイドにあるCookieの読み書きを行う流れになります。
以下はイメージした図になります。
クライアント(ブラウザ)でCookie付きリクエスト
↓
ProxyがgetAll()でCookieを読み取る
↓
setAll()でSupabaseのレスポンスを再生成して、Cookieを書き込む
↓
クライアント(ブラウザ)で新しいCookieを受け取る
このようにProxyは門番のように、クライアントとサーバーの間に入ってうまく中継してくれていると感じました。
【後半部分】 getClaimsについて
以下公式の引用です。
getClaimsは、サーバーのJSON Web Key Setエンドポイント(/.well-known/jwks.json多くの場合キャッシュされている)に対してJWTを検証します。
簡単に言えば、ログインのトークンに不正がないかキャッシュされているエンドポイントから調べるよということです。キャッシュされているものをチェックするので、簡単にローカルチェックというニュアンスがいいかなと思っています。なので、きちんと調べるときはサーバーでのチェックのgetUser()の方が良いと思っています。
getClaimsとgetUserの対立も検索の予測変換で上位にあがっていたので、またいつか記事でまとめます。
追記しました!
↓
getClaimsで取得したデータから、ログインの情報を定義して、それぞれのページに遷移したときの挙動をまとめています。自分は頭の中でごちゃごちゃしたので、ページのルーティングは手で書き出すと整理しやすかったです。
まとめ
Proxyはmiddlewareという呼び方をしている記事もいくつか見かけました。どちらでも良さそうですが、Next.jsではファイル名はProxyとしていくようです。
今後もProxyについては利用していくと思います。
ネットワークの知識などがもっとあれば、理解が深まりそうなので、その分野にも学習の範囲を広げていけたらと思っています。