Skyroc Admin React
专题能力

登录认证

理解登录页结构、useInitLogin/useAuth、token 持久化、用户初始化和退出登录链路

这一页解释 apps/admin 的登录认证链路。重点是把登录页、token 持久化、用户信息初始化、菜单初始化、守卫和退出登录放到同一条链路里看。

当前认证状态由 features/auth 维护,接口由 service/api/auth 提供,路由守卫通过 TanStack Router context 消费 useAuth() 的结果。

只想查 useAuth() / useInitLogin()setAuth / getToken 等的签名和返回值,直接看 登录认证 API

适用场景

场景重点位置
修改账号密码登录页pages/(auth)/login/index.tsxuseInitLogin()
修改登录页布局和动画pages/(auth)/login/layout.tsxpages/(auth)/login/modules/Header.tsx
接真实登录接口service/api/auth/api.tsAUTH_URLS.LOGIN
调整 token 存储和刷新features/auth/use-auth.tsfeatures/auth/shared.tsservice/adapter.ts
理解登录后为什么要初始化菜单useAuth().initAuth()useMenus().initMenus(data)
实现退出登录pages/(auth)/login-out.tsxclearAuth()
排查未登录跳转features/router/guard.tsrouting/guards.mdx
排查 403 权限features/menus/permissions.tsrouting/permission.mdx

当前实现位置

文件职责
apps/admin/src/pages/(auth)/login/layout.tsx登录路由 layout,校验 redirect search,已登录时回到 redirect 或首页,并渲染登录卡片与动画背景。
apps/admin/src/pages/(auth)/login/index.tsx账号密码登录页。提交 Ant Design Form 后调用 useInitLogin().login()
apps/admin/src/pages/(auth)/login/code-login.tsx验证码登录占位页面。
apps/admin/src/pages/(auth)/login/register.tsx注册页面入口。
apps/admin/src/pages/(auth)/login/reset-pwd.tsx重置密码页面入口。
apps/admin/src/pages/(auth)/login-out.tsx退出登录路由。执行 context.clearAuth(),再跳转到 /login
apps/admin/src/features/auth/use-login.ts登录提交流程。负责防重复提交、调用登录接口、写 token、初始化用户和跳转。
apps/admin/src/features/auth/use-auth.ts认证状态核心。维护 token、用户信息读取、菜单初始化、退出清理和首页计算。
apps/admin/src/features/auth/shared.tstoken 读取和认证缓存清理 helper。
apps/admin/src/service/api/auth/*登录、用户信息和刷新 token 接口模块。
apps/admin/src/service/adapter.ts请求层认证适配器。为 @skyroc/service 提供 token、refreshToken、跳转登录和错误提示能力。

登录链路

账号密码登录的核心链路是:

pages/(auth)/login/index.tsx
  -> Ant Design Form onFinish
  -> useInitLogin().login(params)
  -> useLoginMutation()
  -> fetchLogin()
  -> setAuth(token, refreshToken)
  -> initAuth()
  -> initMenus(userInfo)
  -> navigate(redirect 或 /)

useInitLogin() 会用 useLoading() 防止同一轮登录重复提交:

async function login(params: Api.Auth.LoginParams, redirect = true) {
  if (loading) return;

  startLoading();
  toLogin(params, { onSuccess, onError });
}

登录接口成功后,先写入 token:

setAuth(data);

然后初始化当前用户:

const info = await initAuth();

只有用户信息和菜单初始化成功后,才会跳转页面并展示登录成功通知。

登录页结构

登录页由 layout route 和子页面组成:

路由文件URL职责
pages/(auth)/login/layout.tsx/login登录页外壳、背景、卡片、动画和已登录重定向。
pages/(auth)/login/index.tsx/login/账号密码登录表单。
pages/(auth)/login/code-login.tsx/login/code-login验证码登录入口。
pages/(auth)/login/register.tsx/login/register注册入口。
pages/(auth)/login/reset-pwd.tsx/login/reset-pwd重置密码入口。

layout.tsx 使用 zod 校验 search:

const LoginSearchSchema = z.object({
  redirect: z.string().startsWith('/').optional()
});

这保证 redirect 只能是站内路径,避免把登录完成后的跳转交给任意外部地址。

token 持久化

认证缓存使用 localStg,其前缀来自:

VITE_STORAGE_PREFIX=SR_

setAuth() 同时更新 Jotai atom 和本地缓存:

export const setAuth = (data: Api.Auth.LoginToken) => {
  globalStore.set(authAtom, { token: data.token });

  localStg.set('token', data.token);
  localStg.set('refreshToken', data.refreshToken);
};

应用首次加载时,use-auth.ts 会从本地缓存读取初始 token:

const initToken = getToken();

因此刷新页面后,只要 token 仍在本地缓存中,isLoggedIn 就会先变为 true,随后由 root route 或后台守卫触发 initAuth() 获取用户信息。

用户初始化

initAuth() 是登录态变成“可进入后台”的关键步骤:

async function initAuth() {
  try {
    const data = await queryClient.ensureQueryData(queryUserInfoOptions());

    await initMenus(data);

    flushSync(() => {
      setState({ initialized: true });
    });

    return data;
  } catch {
    return null;
  }
}

它做三件事:

步骤作用
ensureQueryData(queryUserInfoOptions())请求或复用当前用户信息。
initMenus(data)根据用户信息、静态/动态路由模式生成菜单、首页和授权索引。
setState({ initialized: true })标记认证上下文已初始化。

如果 initAuth() 返回 null,守卫会清理认证状态并跳回登录页。

登录后跳转

登录页支持 redirect search。未登录访问后台页面时,守卫会跳到:

/login?redirect=/manage/user

登录成功后,useInitLogin() 默认会回到 search.redirect,否则回到 /,再由根路由和首页配置决定最终位置。

还有一个用户切换逻辑:

if (!lastLoginUserId || lastLoginUserId !== info.userId) {
  needRedirect = false;

  localStg.remove('globalTabs');
  localStg.remove('lastLoginUserId');
}

如果本次登录用户和上次用户不同,会清掉缓存 tabs,并回到根路径,避免把上个用户的标签页状态带到新用户会话中。

退出登录

退出登录通过路由实现:

/login-out

pages/(auth)/login-out.tsxbeforeLoad 中执行:

context.clearAuth();

throw redirect({ to: '/login', search: redirectPath ? { redirect: redirectPath } : undefined });

clearAuth() 会清理:

清理项作用
queryClient.clear()清掉 React Query 缓存。
setState({ token: '' })清掉当前 token 状态。
clearAuthStorage()清掉 tokenrefreshToken
clearMenus()清掉菜单和授权索引。
cacheTabs()退出时让布局包缓存 tabs 状态。

需要加“退出登录”按钮时,优先导航到 /login-out,不要在按钮里手动复制清理逻辑。

token 刷新和请求层

requestcreateAppRequest() 创建,并通过 antdAdapter 注入认证能力:

adapter 方法作用
getToken()请求前生成 Authorization: Bearer <token>
getRefreshToken()token 过期时拿 refresh token。
fetchRefreshToken()调用 /auth/refreshToken
setAuth()刷新成功后写入新 token。
redirectToLogin()刷新失败或登出 code 时跳到 /login-out
resetAuth()清理 token 缓存。

后端业务 code 由 .env 控制:

VITE_SERVICE_LOGOUT_CODES=8888,8889
VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778
VITE_SERVICE_EXPIRED_TOKEN_CODES=9999,9998,3333

请求层识别到过期 code 后,会共享同一个 refresh promise,刷新成功后重试原请求;刷新失败则跳转登录。

最小接入示例

如果新增一个登录方式,仍然应复用 useInitLogin().login(),让 token 写入、用户初始化和跳转保持一致:

import { Button } from 'antd';

import { useInitLogin } from '@/features/auth/use-login';

const DemoLoginButton = () => {
  const { loading, login } = useInitLogin();

  function handleLogin() {
    login({
      password: '123456',
      userName: 'Admin'
    });
  }

  return (
    <Button loading={loading} type="primary" onClick={handleLogin}>
      登录
    </Button>
  );
};

新增登录接口时,可以扩展 service/api/auth,但成功后要回到同一套 setAuth -> initAuth -> navigate 链路。

常见误区

误区正确做法
登录成功后只写 token,不调用 initAuth()登录后必须初始化用户信息和菜单,否则后台布局和权限索引不完整。
在页面按钮里手动清理 token、query、菜单和 tabs导航到 /login-out 或复用 clearAuth()
为不同登录方式写多套 token 存储统一走 setAuth(),让 Jotai 状态和 local storage 同步。
忽略 redirect search未登录直达后台页面时,登录后应回到原路径。
把 refresh token 逻辑写进页面token 刷新属于请求层,当前入口是 service/adapter.ts@skyroc/service

相关链接

On this page