现在最后还有一个问题:
即使我们已经有:
text
token
user
用户仍然可以手动输入:
text
/pay
直接访问 Pay 页面。
所以我们还需要解决:
未登录用户不能进入受保护页面。
这就是:
路由守卫。
一、为什么需要路由守卫?
假设:
text
/pay
是只有登录用户才能访问的页面。
那么我们希望:
text
已登录 → 可以访问 /pay
未登录 → 跳转 /login
这个判断不属于 Pay 页面自己的业务。
所以我们创建一个专门的组件:
text
src/components/RequireAuth.jsx
它的任务很简单:
保护页面。
二、创建 RequireAuth
代码:
jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/user';
function RequireAuth({ children }) {
const token = useAuthStore(state => state.token);
const location = useLocation();
if (!token) {
return (
<Navigate
to="/login"
state={{ from: location.pathname }}
replace
/>
);
}
return children;
}
export default RequireAuth;
这里最核心的判断:
js
if (!token)
意思就是:
没有登录凭证。
那怎么办?
jsx
<Navigate to="/login" />
直接去登录页。
三、为什么把 Pay 包在 RequireAuth 里面?
路由中:
jsx
<Route
path="/pay"
element={
<RequireAuth>
<Pay />
</RequireAuth>
}
/>
这里可以理解成:
text
访问 /pay
↓
先经过 RequireAuth
↓
检查 token
↓
有 token → Pay
没有 token → Login
所以 RequireAuth 就像一个门。
Pay 页面是门后面的内容。
四、为什么不直接把判断写到 Pay.jsx?
当然也可以。
但是如果以后:
text
Pay
Order
Profile
Admin
Settings
都需要登录。
难道每一个页面都写:
js
if (!token) {
navigate('/login');
}
这样会重复大量代码。
所以我们把这个公共逻辑抽成:
text
RequireAuth
以后所有需要保护的页面都可以复用。
五、登录后为什么要回到原来的页面?
假设用户本来访问:
text
/pay
但是没登录。
于是:
text
/pay
↓
/login
如果登录成功以后永远跳到:
text
/
用户就会感觉:
"我刚才明明想去 Pay,怎么登录完跑首页去了?"
所以 RequireAuth 里记录了:
jsx
state={{ from: location.pathname }}
意思是:
告诉登录页面,我之前是从哪里来的。
六、Login 页面读取 from
登录页面:
js
const location = useLocation();
const from = location.state?.from || '/';
如果用户之前访问的是:
text
/pay
那么:
js
from === '/pay'
如果没有记录来源:
js
from === '/'
七、登录成功以后跳回去
登录成功:
js
const res = await login(formData);
然后:
js
if (res.code === 0) {
setAuth({
token: res.token,
user: res.user
});
navigate(from, {
replace: true
});
}
于是:
text
用户访问 /pay
↓
RequireAuth 检查 token
↓
没有 token
↓
跳到 /login
↓
保存 from = /pay
↓
用户完成登录
↓
服务器返回 Token
↓
setAuth()
↓
navigate('/pay')
↓
进入 Pay
这就形成了完整的登录跳转体验。
八、现在终于可以把整个项目串起来
到这里,这个 demo 的所有主要部分都已经出现了:
text
用户访问受保护页面
↓
RequireAuth 检查 Zustand 中的 token
↓
没有 token → 跳转登录页
↓
用户填写账号密码
↓
Login 调用 user.js 的 login()
↓
Axios 发送 /api/login
↓
Mock 后端验证账号密码
↓
jwt.sign() 生成 Token
↓
Token 返回前端
↓
Zustand 保存 token 和 user
↓
同时保存到 localStorage
↓
后续 Axios 请求自动添加 Authorization
↓
Mock 后端读取 Bearer Token
↓
jwt.verify() 验证 Token
↓
验证成功 → 返回受保护数据
这条链就是整个 demo 的核心。
九、最后再区分几个非常容易混淆的东西
这个 demo 里出现了很多概念,刚学的时候很容易全部混到一起。
API 层
例如:
text
src/api/user.js
src/api/repo.js
解决的是:
前端怎么组织接口请求。
Mock
例如:
text
mock/user.js
解决的是:
开发阶段没有真实后端时,谁来模拟后端接口。
JWT
例如:
js
jwt.sign()
jwt.verify()
解决的是:
服务器怎么给用户签发身份凭证,以及怎么验证这个凭证。
Axios 拦截器
例如:
js
instance.interceptors.request.use(...)
解决的是:
怎么让每次请求自动携带 Token,而不是一个接口一个接口地写。
Zustand
例如:
js
useAuthStore(...)
解决的是:
React 应用内部怎么共享当前登录状态。
localStorage
解决的是:
刷新页面之后,Token 仍然能够被找回来。
RequireAuth
解决的是:
没有登录的用户能不能进入某个页面。
最后:你真正应该记住的不是五篇文章
这个 demo 如果只记 API 名字,很快就会乱。
真正应该记住的是下面这几个"为什么":
为什么需要 API 层?
因为不能让每个 React 页面自己管理请求细节。
为什么需要 Mock?
因为前端开发时,需要一个可以模拟后端的环境。
为什么需要 Token?
因为 HTTP 是无状态的,服务器需要一个凭证知道"你是谁"。
为什么需要 Axios 拦截器?
因为每个请求都需要 Token,公共逻辑应该统一处理。
为什么需要 Zustand?
因为多个 React 组件都需要知道当前用户是谁,登录状态属于全局状态。
为什么需要 localStorage?
因为 React 内存状态刷新后会丢,需要一个持久化的位置保存 Token。
为什么需要 RequireAuth?
因为"有没有 Token"不仅影响请求,也影响用户能不能进入某个页面。
最终,你应该能够自己说出这一句话:
用户登录成功后,服务器使用 JWT 签发 Token,前端把 Token 保存下来并放进登录状态 Store;之后 Axios 拦截器会在请求发送前自动把 Token 放进 Authorization Header,服务器收到请求后通过
jwt.verify()验证身份,而 React Router 再利用全局登录状态决定用户能不能访问受保护页面。
这句话如果你能真正讲明白,这个 demo 的主线基本就已经吃透了。