Nuxt3でアクセストークンを使った簡易な認証フローの構築をしてみます。
簡易な認証フローでは、route middlewareを使います。route middlewareはNuxt2にも同様にあった仕組みです。サーバーサイド(SSRサーバ初回アクセス時)、クライアントサイドで動作し、ページ遷移時に認証などのフィルタを挟んだりすることができます。ページコンポーネントに以下のように記述することで、ミドルウェアを宣言的に適用することができます。
// 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が上手いこと処理をしてくれます(具体的には後述の通り)。
// 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
今回は、トークンを返すだけの簡易な処理としています。
export default defineEventHandler((event) => {
return {
accessToken: "dummy token",
};
});
server/api/posts.ts
ログイン後に何らかの投稿記事一覧を返す処理とします。リクエストヘッダから、Authorizationヘッダを読みトークンを確認します。トークンがあれば、ひとまず記事一覧を返すとします。
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にも書き込まれます。
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
<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
<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


コメント