Nuxt3でアクセストークンを使った簡易な認証フローの構築をしてみます。
簡易な認証フローでは、route middlewareを使います。route middlewareはNuxt2にも同様にあった仕組みです。サーバーサイド(SSRサーバ初回アクセス時)、クライアントサイドで動作し、ページ遷移時に認証などのフィルタを挟んだりすることができます。ページコンポーネントに以下のように記述することで、ミドルウェアを宣言的に適用することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Nuxt2 // ページやレイアウトコンポーネントで指定 export default { middleware: ['auth'] } // Nuxt3 // ページコンポーネントで指定 <script setup> definePageMeta({ middleware: ["auth"] // or middleware: 'auth' }) </script> |
参照:ミドルウェアディレクトリ
なお、server/middlewareという仕組みもありますが、これはサーバーリクエストごとに動作するミドルウェアです。ドキュメントにもあるように、何らかのチェックやヘッダの操作、ロギングなどに使用する用途のようです。expressでrouteをマウントしても呼ばれます。Nuxt3の内部で使われているh3 httpフレームワークのミドルウェアとして登録されます。
そのため今回は、route middlewareの仕組みを使って構築してみます。
テスト環境
- nuxt@3.0.0-rc.8
今回のフロー
以下のような構成にしてみます。nuxt-auth moduleもcookieを使った同様の機構があります。
- トークンはcookieに保存、ログイン時にcookieに書き込む。クライアント側でもread/writeするのでHttpOnly属性はなし。
- SSR時は、cookieからトークンを読み出して使う。
- route middlewareでは、cookieから値を読み出しログイン済みかどうかをチェックする。
この他、以下の仕組みを使うので補足します。
route middleware
route middlewareですが、middlewareディレクトリに配置すると自動でインポートされます。.global
というサフィックスをつけることで、全体に対して適用することができます。
- middleware/auth.ts
- definePageMetaでページコンポーネントに対してミドルウェアを適用する
- middleware/auth.global.ts
- 全体に渡ってミドルウェアが適用される
useCookie
useCookieというcomposableライブラリを使用します。useCookieは、サーバサイドとクライアントサイドで使えます。useCookieによる返り値に対しvalueを書き込みすることで、裏でNuxtが上手いこと処理をしてくれます(具体的には後述の通り)。
1 2 3 4 5 6 7 8 9 10 |
// cookieはRefオブジェクト const cookie = useCookie<string | null>('access_token', cookieOptions) // クライアントサイド // document.cookieに書き込みされる // 具体的にはcookieはwatch apiの監視対象となっているので値の更新を検知して書き込まれます // サーバサイド // event.req.headers.cookieに書き込みされる // app:rendered、app:redirectedフック実行のタイミングでhttpヘッダに書き込まれる cookie.value = accessToken |
これにより、SSR時でもアクセストークンを取得し、フェッチリクエスト時にヘッダにトークンを埋め込むことができるようにします。
クライアントサイドの動作
クライアントサイドでは、document.cookieより指定したcookieを取得します。そのため、HttpOnlyが指定されたcookieは使えません。useCookieにより得たcookieはrefオブジェクトとなっており、valueに値を書き込むことでdocument.cookieにシンクされます。
サーバサイドの動作
サーバサイドでは、フックのタイミングでレスポンスヘッダにcookieが書き込まれます。
route middlewareを使った例
では、実際に構築してみます。フローを確認するためのサンプルなので、エラーチェックやその他バリデーションなど厳格さは欠いていますことご容赦ください。
server/api/login.ts
今回は、トークンを返すだけの簡易な処理としています。
1 2 3 4 5 |
export default defineEventHandler((event) => { return { accessToken: "dummy token", }; }); |
server/api/posts.ts
ログイン後に何らかの投稿記事一覧を返す処理とします。リクエストヘッダから、Authorizationヘッダを読みトークンを確認します。トークンがあれば、ひとまず記事一覧を返すとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import { getRequestHeader } from "h3"; import consola from "consola"; export default defineEventHandler((event) => { const token = getRequestHeader(event, "Authorization"); consola.info(`token: ${token}`); // サンプルとしてtokenの有無だけチェック if (!token) { throw createError({ statusCode: 401, statusMessage: "Not authorized", }); } // TODO: validate(token) return { posts: [ { title: "title1", content: "test content", author: "tarou" }, { title: "title2", content: "test content2", author: "tarou" }, { title: "title3", content: "test content3", author: "tarou" }, ], }; }); |
middleware/auth.global.ts
useCookieを使っています。ログインに成功すると、cookieに値を書き込みます。同時にdocument.cookieにも書き込まれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import type { Ref } from "vue"; export const useAuth = () => { const cookie = useCookie("access_token"); // サンプルではcookieの存在有無だけ // TODO: validate const initialValue = cookie.value ? true : false; const loggedIn = useState("loggedIn", () => initialValue); const login = (loggedIn: Ref<boolean>) => async () => { const data = await $fetch("/api/login"); cookie.value = data.accessToken; loggedIn.value = true; return true; }; const logout = (loggedIn: Ref<boolean>) => async () => { loggedIn.value = false; cookie.value = null; }; const getToken = () => { return cookie.value; }; return { loggedIn, getToken, login: login(loggedIn), logout: logout(loggedIn), }; }; |
pages/login.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<script setup> const login = () => { const { login } = useAuth(); login().then(() => { const router = useRouter(); router.replace("/dashboard"); }); }; </script> <template> <div> <button @click="login">Login</button> </div> </template> |
pages/dashboard.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<script setup lang="ts"> const { getToken } = useAuth(); // cookieから読み出し const { data } = await useFetch("/api/posts", { baseURL: "http://localhost:3000", headers: { Authorization: `Bearer ${getToken()}`, }, }); const logout = () => { const { logout } = useAuth(); const router = useRouter(); logout(); router.replace("/login"); }; </script> <template> <div> <template v-if="data?.posts"> <ul v-for="(post, i) in data.posts" :key="i"> <li>{{ post.title }}</li> </ul> </template> <div> <button @click="logout">logout</button> </div> </div> </template> |
以下のような感じで動きます。
参考リンク
- Nuxt 3 の Route Middleware で簡単な認証フローを構築する – Zenn
- https://v3.nuxtjs.org/guide/directory-structure/middleware
コメント