airi 前端鉴权指南:用 VueUse useAuth 在 Vue 3 中响应式绑定 Firebase Auth 登录状态
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
useAuth是 VueUse 官方 Firebase 集成(@vueuse/firebase)提供的响应式鉴权绑定,它把 Firebase Auth 的登录状态封装成user与isAuthenticated两个响应式值,让 Vue 组件可以声明式地响应登录、登出与令牌刷新事件。本文以 airi 仓库中 vueuse-functions 技能库 的 useAuth 参考文档 为主体,完整讲解其用法、返回值、底层监听机制与类型声明,并结合 airi 仓库自研的响应式鉴权实现(useAuthStore)做源码级对照,帮助你掌握"用响应式状态驱动 UI 鉴权"这一通用设计模式。
一、技能定位:useAuth 在 vueuse-functions 中的角色
在 airi 仓库的.agents/skills/vueuse-functions/技能库中,所有 VueUse 组合式函数按功能分类维护了各自的参考文档,useAuth被归类在@Firebase分组下,与该分组的 useFirestore(响应式 Firestore 绑定)、useRTDB(响应式 Realtime Database 绑定)并列,三者同属@vueuse/firebase集成包。
根据 SKILL.md 中的调用规则(Invocation)标注,useAuth的调用级别为EXTERNAL,即:仅当用户已经安装了@vueuse/firebase与firebase外部依赖时才应当使用;否则需要先评估是否真正需要该依赖,再决定是否安装。这是技能库对"外部集成类组合式函数"的通用约束——避免为了一句便利的响应式封装而引入整个 SDK。
### @Firebase | 函数 | 描述 | Invocation | |--------------|-----------------------------------|------------| | useAuth | Reactive Firebase Auth binding | EXTERNAL | | useFirestore | Reactive Firestore binding | EXTERNAL | | useRTDB | Reactive Firebase Realtime Database binding | EXTERNAL |从类型声明来看,useAuth被标注了@__NO_SIDE_EFFECTS__,这是一个面向打包器的 tree-shaking 注解,表示调用该函数本身不会产生模块级副作用,从而允许构建工具在未使用该导入时安全地将其从产物中剔除。
二、核心用法:把 Firebase Auth 状态变成响应式数据
useAuth的使用方式非常简洁:它接收一个由 Firebase SDK 创建的Auth实例,返回响应式的user与isAuthenticated。参考文档给出了一个可直接运行的最小示例(Vue 3<script setup>语法):
<script setup lang="ts"> import { useAuth } from '@vueuse/firebase/useAuth' import { initializeApp } from 'firebase/app' import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth' const app = initializeApp({ /* config */ }) const auth = getAuth(app) const { isAuthenticated, user } = useAuth(auth) const signIn = () => signInWithPopup(auth, new GoogleAuthProvider()) </script> <template> <pre v-if="isAuthenticated">{{ user }}</pre> <div v-else> <button @click="signIn"> Sign In with Google </button> </div> </template>拆解这个示例,可以得到完整的调用链:
- 初始化 Firebase 应用:
initializeApp({ /* config */ })需要传入你的 Firebase 项目配置(apiKey、authDomain 等),通常在应用入口处完成一次,再通过模块导出复用; - 获取 Auth 实例:
getAuth(app)从应用中取出认证实例,它是后续所有鉴权操作的入口; - 响应式绑定:
useAuth(auth)返回的isAuthenticated与user会随认证状态自动更新,模板中无需手动刷新; - 发起登录:
signInWithPopup(auth, new GoogleAuthProvider())是 Firebase 提供的弹出式 Google 登录方法——注意登录动作本身仍由 Firebase SDK 驱动,useAuth只负责"感知"并传播状态变化。
在真实业务中,你通常还需要处理登出与用户信息展示。可以在同一组件中直接组合 Firebase 原生的signOut与useAuth的返回值,形成完整的登录生命周期:
<script setup lang="ts"> import { useAuth } from '@vueuse/firebase/useAuth' import { getAuth, signInWithPopup, signOut, GoogleAuthProvider } from 'firebase/auth' const auth = getAuth() const { isAuthenticated, user } = useAuth(auth) const signIn = () => signInWithPopup(auth, new GoogleAuthProvider()) const signOutUser = () => signOut(auth) </script> <template> <div v-if="isAuthenticated && user"> <p>{{ user.displayName ?? user.email }}</p> <button @click="signOutUser">Sign Out</button> </div> <button v-else @click="signIn">Sign In</button> </template>user是可空引用(null表示未登录),因此模板中与v-if组合使用时需要做空值守卫;而isAuthenticated是布尔型计算属性,可直接用于路由守卫、导航栏按钮、页面拦截等场景的条件分支。
三、返回值详解
参考文档用一张表明确了useAuth的两个返回值,其类型与语义如下:
| 名称 | 类型 | 说明 |
|---|---|---|
user | Ref<User \| null> | 当前 Firebase 用户对象;未认证时为null |
isAuthenticated | ComputedRef<boolean> | 当前是否处于已认证状态 |
需要强调两个关键点:
user是Ref,isAuthenticated是ComputedRef。在<script setup>顶层解构后,模板中可直接使用(Vue 会自动解包);在 JS 逻辑中则需通过.value访问,或借助storeToRefs、toRefs等工具维持响应式。isAuthenticated是从user派生的,即user.value !== null。这意味着它天然与user保持一致,不存在"user 已有值但 isAuthenticated 仍为 false"的状态撕裂问题。
四、底层原理:onIdTokenChanged 监听器
useAuth之所以能做到"状态自动更新",核心在于它内部注册了 Firebase Auth 的onIdTokenChanged监听器。参考文档明确指出:
The composable automatically updates when the user's ID token changes (including sign-in, sign-out, and token refresh events) using Firebase's
onIdTokenChangedlistener.
onIdTokenChanged是 Firebase Auth 提供的状态监听 API,它会在以下事件发生时触发回调:
- 登录(sign-in):包括
signInWithPopup、signInWithRedirect、signInWithEmailAndPassword等所有登录方式完成之后; - 登出(sign-out):调用
signOut后用户对象变为null; - ID Token 刷新(token refresh):Firebase 会周期性地自动刷新用户的 ID Token(默认约每小时),此时也会触发监听——这正是
useAuth相比"只在登录/登出时手动更新一次"的朴素实现更可靠的原因:即使会话在后台被静默刷新,响应式状态也始终与真实认证态同步。
使用useAuth之后,组件无需自己管理监听器的注册与解绑——这正是 VueUse 组合式函数"依赖 Vue 组件作用域自动清理"的设计惯例(组件卸载时随 scope dispose 一并解除),避免了传统onAuthStateChanged手动注册/卸载容易造成的内存泄漏问题。
五、类型声明逐段解析
参考文档给出了useAuth的完整 TypeScript 类型声明,理解它能帮助你正确地消费user对象。整理后其签名结构为:
export declare function useAuth(auth: Auth): { isAuthenticated: ComputedRef<boolean> user: Ref<User | null> }其中User是对 Firebase 用户模型的静态类型描述,包含以下关键字段与方法:
| 成员 | 类型 | 用途 |
|---|---|---|
uid | string | 用户唯一标识,跨会话稳定 |
email/displayName/phoneNumber/photoURL | string \| null | 用户的邮箱、显示名、电话、头像链接(可空) |
emailVerified | boolean | 邮箱是否已验证,常用于"未验证禁止访问"策略 |
isAnonymous | boolean | 是否匿名用户(匿名登录场景) |
metadata | { creationTime?, lastSignInTime? } | 账号创建时间与最近登录时间 |
providerData | 数组 | 各登录提供方(Google、GitHub 等)对应的资料快照 |
refreshToken | string | 刷新令牌,用于换取新的 ID Token |
tenantId | string \| null | 多租户(Firebase Identity Platform)场景下的租户 ID |
delete() | () => Promise<void> | 删除当前用户账号 |
getIdToken(forceRefresh?) | () => Promise<string> | 获取(或强制刷新)ID Token,是携带 Bearer 请求头访问受保护 API 的常用方式 |
getIdTokenResult(forceRefresh?) | () => Promise<IdTokenResult> | 获取 ID Token 及其声明信息 |
reload() | () => Promise<void> | 重新加载用户资料(如邮箱验证后刷新状态) |
toJSON() | () => object | 序列化为纯对象 |
实际使用中,最常消费的是uid、email、displayName、photoURL(用于展示头像与昵称)与getIdToken()(用于向自己的后端接口传递凭证)。
六、源码级对照:airi 仓库的响应式鉴权实现
有趣的是,airi 仓库本身并未使用 Firebase Auth,而是采用了自研的OIDC + Better Auth鉴权栈——但它在设计上与useAuth遵循着完全相同的"响应式状态驱动 UI"哲学。对照阅读可以加深你对本主题的理解。
6.1 相同的响应式骨架:user + isAuthenticated
airi 的鉴权核心是 packages/stage-ui/src/stores/auth.ts 中的useAuthStore(Pinia store)。它暴露了与useAuth同构的状态对:
const user = ref<User | null>(null) const session = ref<Session | null>(null) const isAuthenticated = computed(() => !!user.value && !!session.value)与 VueUseuseAuth一样:user是Ref,isAuthenticated是派生的ComputedRef。只是 airi 的判定更严格——要求用户对象与服务端会话对象同时存在才视为已认证。
6.2 监听状态迁移:watch(isAuthenticated)
useAuth通过onIdTokenChanged回调感知状态变化;airi 则用 Vue 的watch监听isAuthenticated的翻转,并在翻转时执行认证/登出钩子(见 auth.ts#L369-L394):
watch(isAuthenticated, async (authenticated, wasAuthenticated) => { if (authenticated) { void updateCredits() needsLogin.value = false if (!wasAuthenticated) await dispatchHooks(authenticatedHooks, 'auth hook error') } else { credits.value = 0 if (wasAuthenticated) await dispatchHooks(logoutHooks, 'logout hook error') } }, { immediate: true })这段实现展示了响应式鉴权的两个实用技巧:
{ immediate: true }:store 初始化时立即执行一次,保证"注册钩子时若已登录则立刻触发"(对应 onAuthenticated 中的提前触发逻辑);- 钩子机制:用
authenticatedHooks/logoutHooks数组注册回调,业务模块通过onAuthenticated/onLogout订阅登录/登出事件,解耦了"状态变化"与"副作用执行"。
6.3 状态消费的 UI 侧印证
在组件层,airi 的鉴权按钮同样把状态解构成响应式变量供模板消费。以 controls-island-auth-button.vue 为例:
const { isAuthenticated, user, needsLogin, credits } = storeToRefs(authStore)这与useAuth示例中"解构出isAuthenticated和user,在模板中v-if分支渲染"的模式完全一致——响应式鉴权状态让 UI 的"登录前/登录后"切换变成纯粹的声明式渲染,无需手动管理任何中间状态。
6.4 更进一步的工程化:401 自动刷新
useAuth依赖 Firebase 自动刷新 ID Token;airi 则在 packages/stage-ui/src/libs/auth-fetch.ts 的authedFetch中实现了"401 → 单飞刷新令牌 → 重放请求"的安全网机制。它之所以需要这层兜底,正如代码注释所说明的:时钟偏移、挂起的标签页、刷新后的竞态都可能让过期的 Bearer Token 泄漏出去(auth-fetch.ts#L5-L19)。这个思路同样适用于 Firebase 场景——即使onIdTokenChanged会传播刷新事件,对后端请求仍建议在收到 401 时执行一次user.getIdToken(true)强制刷新后重试。
七、典型应用场景与注意事项
基于以上原理,useAuth最常见的落地场景包括:
- 导航栏登录态切换:
v-if="isAuthenticated"决定展示"用户头像/登出"还是"登录按钮",与 airi 中 HeaderAvatar.vue 的做法一致; - 路由守卫与页面拦截:在路由前置守卫中读取
isAuthenticated.value,未登录则重定向到登录页——airi 的 onboarding.vue 正是用watch([isAuthenticated, closeRequestId])实现登录引导的; - 用户信息展示:解构
user后读取displayName、photoURL、email等字段渲染头像与昵称; - 受保护 API 调用:用
user.getIdToken()获取 ID Token 作为 Bearer 凭证请求后端。
使用时的注意事项:
user为null时访问其属性会报错,模板中务必用v-if/v-else分流或可选链(user?.displayName);isAuthenticated是计算属性,不要直接对它赋值;需要手动重置状态时应通过signOut完成;useAuth要求前置安装firebase与@vueuse/firebase(EXTERNAL 调用级别),若项目尚未使用 Firebase,请先评估是否值得为响应式封装引入整套 SDK;- 当页面同时存在多个组件调用
useAuth时,它们共享同一个Auth实例的监听结果,状态保持一致,不会出现互相覆盖的问题。
结语
useAuth以极小的 API 面(一个函数、两个返回值)解决了 Firebase 前端鉴权中最常见的痛点——登录状态的响应式同步。它的设计精髓在于:把"监听外部异步状态"这一样板逻辑封装进组合式函数,让组件只关心"状态是什么"而不用关心"状态怎么来的"。airi 仓库自研的useAuthStore虽然在技术上选择了 OIDC + Better Auth 而非 Firebase,但其user/isAuthenticated/watch(isAuthenticated)的结构与useAuth如出一辙,印证了这一模式在真实项目中的可迁移性。掌握useAuth,也就掌握了在任意 Vue 3 项目中搭建响应式鉴权层的通用范式。
参考文档:.agents/skills/vueuse-functions/references/useAuth.md | 技能索引:.agents/skills/vueuse-functions/SKILL.md | airi 对照实现:packages/stage-ui/src/stores/auth.ts
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考