实现思路
六步旅程
- 想清楚:login/register API 约定、token 存储策略(localStorage,键
'token',与模块 0 拦截器注释一致)、受保护路由清单(/cart) - 静态骨架:/login、/register 路由 + 表单页渲染
- 拆分组件:LoginForm / RegisterForm(UI + 声明式校验内聚,emit submit;业务归父级/store)
- 接入数据:mock 认证 API + auth store(token/user + localStorage 持久化)+ 拦截器自动附带 token(模块 0 伏笔兑现)
- 状态管理:守卫落地——Vue
beforeEach+ meta vs ReactRequireAuth包装器 + redirect 跳回来源页 + 登出 - 验证与笔记:登录/登出/未登录重定向全流程测试
认证数据流
LoginForm(收集 + 声明式校验)
→ emit submit(values)
→ 页面调 authStore.login(values)
→ mock API 校验 → 签发 token + user
→ store 状态更新 → watch / subscribe → localStorage 持久化(变化时写)
→ 请求拦截器:每次请求自动附带 Authorization: Bearer <token>
→ 访问受保护路由 → 守卫检查 isLoggedIn
→ 未登录:重定向 /login?redirect=来源页
→ 登录成功:读取 redirect 跳回
关键设计约定
- **token 存 localStorage**(键 `'token'`)——简单、刷新不丢;真实项目推荐 httpOnly cookie(见面试考点 3) - **注册成功即登录**(返回 token)——省一次跳转,两页成功处理一致 - **权限边界**:浏览商品公开;涉及「我的数据」(购物车)才受保护关键差异对比
| 项 | Vue 版 | React 版 | 为什么 |
|---|---|---|---|
| 1️⃣ 守卫形态 | beforeEach 全局导航钩子(框架内置,注册一次管所有路由)+ meta.requiresAuth 标记 |
RequireAuth 包装器组件(手动组装,包住受保护路由) |
Vue Router 内置守卫能力(配置式哲学);React Router「路由即组件」——权限控制本质是条件渲染(没登录就不渲染目标页),靠组件组装 |
| 2️⃣ 拦截方式 | 守卫返回路由位置对象 = 重定向(返回 undefined 放行 / false 取消) | 渲染 <Navigate to="..." replace />(声明式重定向组件) |
钩子里用返回值表达意图;组件树里用组件表达 |
| 3️⃣ 表单数据管理 | antdv::model="formState" + v-model:value——值由你的响应式对象管 |
antd:Form.Item 的 name 注册,值由 Form 内部管理,onFinish 把 values 给你(不需要 useState 管字段) |
React 显式状态哲学下逐字段 useState 太啰嗦,antd 用 Form 实例收口;Vue 模板双向绑定是语言级能力,值放响应式对象自然 |
| 4️⃣ 校验规则 | 声明式 rules(集中定义在表单组件) |
声明式 rules(写在 Form.Item 上) |
两边一致:规则与校验时机解耦——Form 接管时机(失焦/提交),@finish / onFinish 只在全部通过后触发 |
| 5️⃣ 确认密码取值 | validator 直接读同组件的 formState.password |
dependencies={['password']} + getFieldValue('password') |
Vue 的值在响应式对象里直接可读;React 的值在 Form 内部 store,要声明字段联动 + API 取值 |
| 6️⃣ isLoggedIn 派生 | store 里 computed(() => token !== '') |
不存字段,selector 派生 useAuthStore((s) => s.token !== '') |
派生状态不存储:Vue computed 标准写法;Zustand 无计算属性,使用处计算(呼应模块 3 的派生状态概念) |
| 7️⃣ 登录态持久化 | watch([token, user]) 变化时写 |
useAuthStore.subscribe 变化时写 |
复用模块 3 的持久化三要素:时机(变化时写、初始化时读)、序列化、防御 |
关键 API / 配置
router.beforeEach(to => ...)(Vue):全局前置守卫。返回值三语义:undefined/true放行、false取消导航、路由位置对象 = 重定向;meta(Vue):路由元信息——把「路由的附加数据」(如requiresAuth)挂在路由配置上,守卫按它判断;<Navigate>(React Router):声明式重定向组件,渲染即跳转(replace不留历史记录);useLocation(React Router):拿当前路径(守卫重定向时作为 redirect 参数);useSearchParams(React Router):读 query 参数(登录页读 redirect);- antd Form 的
rules+validator:声明式校验;自定义校验器返回 Promise(resolve 通过 / reject 报错); dependencies+getFieldValue(antd React):字段联动校验(确认密码依赖密码字段);message:成功/失败轻提示(登录成功 / 用户名或密码错误);config.headers.Authorization = 'Bearer <token>':附带 token 的标准 HTTP 格式(Bearer 认证方案)。
踩过的坑
- 守卫里 useAuthStore 的调用时机:现象(若在
router/index.ts模块顶层调useAuthStore(),报no active Pinia)→ 原因(模块加载早于main.ts里的app.use(pinia),调用时还没有活跃的 Pinia 实例——模块 0 的坑重演)→ 解决(在守卫回调内部调用——导航发生时应用已挂载、Pinia 已激活)。教训:组件外用 store 永远要问「调用时机在 pinia 激活之前还是之后」。 - mock 用户库刷新丢失:现象(注册的新账号刷新页面后登录不上)→ 原因(mock 用户库是内存变量,页面刷新即重置)→ 解决(用户库持久化到 localStorage——复用模块 3 的持久化三要素;和
cart-items同一模式)。
面试考点
1. React 为什么没有路由守卫?怎么实现等价效果?
一句话:React Router 的哲学是「路由即组件」——权限控制本质是条件渲染(没登录就不渲染目标页、渲染重定向),没有全局钩子就用包装器组件组装。
展开:
RequireAuth包住受保护路由:内部检查登录态,不通过渲染<Navigate>;- 对比 Vue 的
beforeEach(框架内置全局钩子 + meta 标记); - 两种都合法,差异源自设计哲学:框架提供能力(Vue)vs 一切皆组件(React)。
2. beforeEach 和 RequireAuth 各自的取舍?
一句话:beforeEach 集中管理(一个函数管所有路由,适合权限矩阵复杂的项目);RequireAuth 就地声明(受保护路由显式包一层,一眼看出哪些页面要守)。
展开:
- beforeEach + meta:权限规则集中、可做复杂逻辑(角色矩阵、白名单);劣势是规则分散在 meta 配置里,路由多了要维护;
- RequireAuth:类型安全、显式、可组合(可嵌套);劣势是忘了包就不守——没有集中检查点;
- 真实项目两种都可能用(全局兜底 + 页面级细粒度)。
3. token 存哪?三种存储对比?
一句话:localStorage(持久、XSS 可读)/ sessionStorage(会话级、同样 XSS 可读)/ httpOnly cookie(JS 读不到、最安全、需服务端配合 + CSRF 防护)。
展开:
- 本项目 localStorage:简单 + 刷新不丢,教学够用;
- 真实项目推荐 httpOnly cookie:XSS 偷不走 token;代价是服务端要设置、要防 CSRF;
- 面试加分:能说出 trade-off(便利性 vs 安全性)而不是背一个答案。
4. Axios 拦截器做了哪四件事?
一句话:①baseURL 统一 ②请求拦截器自动附带 token ③响应拦截器统一错误处理(HTTP 层)④业务码统一检查(code !== 0 拒绝)。
展开:
- 为什么在拦截器做:统一(一处生效)、防漏(忘写也不漏)、可替换(换 httpOnly cookie 只改这里);
- 追问「响应拦截器还能做什么」:401 统一跳登录(token 失效处理)、错误日志上报。
5. 声明式校验 vs 手写校验?
一句话:rules 声明「规则」,Form 接管「校验时机」(失焦/提交),finish/onFinish 只在全部通过后触发——规则与时机解耦。
展开:
- 手写校验 = 提交时一堆 if:规则散落、时机手工管理、漏校验难发现;
- 声明式:规则集中、可读、antd 自动在正确时机校验并展示错误;
- 追问自定义校验:validator 返回 Promise(antd React resolve/reject)/ 抛错(antdv)。
6. 守卫里为什么要在回调内调用 useAuthStore?
一句话:Pinia 的 store 必须在 app.use(pinia) 之后才能在组件外调用;router/index.ts 的模块加载早于 main.ts 的 use(pinia)——顶层调用会报 no active Pinia。
展开:
- 回调的执行时机是「导航发生时」——那时应用已挂载、Pinia 已激活;
- 这是「模块加载顺序」的经典坑(本项目模块 0 首次踩过,这里重演)——组件外用 store 永远要问调用时机。
7. 登录后怎么跳回来源页?
一句话:守卫重定向时把来源路径放进 query(?redirect=/cart),登录成功后读取 redirect 优先跳转,没有则回默认页。
展开:
- 链路:守卫
query: { redirect: to.fullPath }→ 登录页读 query →router.push(redirect); - 安全细节(加分):redirect 要校验只允许站内路径(必须以
/开头、不含//),防止「开放重定向漏洞」(被诱导跳到钓鱼站)。
如果换成另一个框架我会怎么写
1. beforeEach → React 怎么实现「全局」效果?
- Vue:注册一次,所有导航都过,集中不漏;
- React Router:没有全局钩子——每个受保护路由显式包 RequireAuth(忘了包 = 漏守);或自己组装「集中式守卫」(路径白名单 + 顶层守卫组件);
- 效果差异:集中式不漏但规则集中维护;包装器显式但靠自觉。真实 React 项目常两者结合(布局级守卫 + 敏感页面单独包)。
2. RequireAuth → Vue 里等价物?
- Vue 也能组件包装(AuthGuard + slot 或嵌套路由 +
router-view),但惯用法是 beforeEach + meta; - 对比结论:Vue 给了两种选择(内置守卫更惯用),React 只有一种(组件组装)——内置能力 vs 组装能力,选型时优先用框架惯用法。
3. antd 两端 Form API 差异的根因?
- React 版:Form 内部管理值——因为 React 受控表单逐字段 useState 太啰嗦,antd 用 Form 实例收口,
onFinish时交付 values; - Vue 版:模板双向绑定(v-model)是框架级能力,值放在你的响应式对象里自然;
- 本质:React 的「状态显式传递」哲学 vs Vue 的「响应式绑定」哲学,在表单这个场景的具体体现。
4. token 放 httpOnly cookie 后,拦截器还要做什么?
- 不用再手动附 Authorization——浏览器对同源请求自动携带 cookie;
- 但要防 CSRF(SameSite 属性 / CSRF token)——安全模型从「防 XSS 偷 token」变成「防跨站伪造请求」;
- 拦截器的角色转变:从「附带 token」变成「统一处理 401 跳登录」;
- 面试加分:说清迁移不只是换存储位置,而是安全对抗面的变化(XSS ↔ CSRF)。