Refine v5 实战:使用 Mantine 构建自定义认证系统(AuthProvider 完整指南)
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文以 Refine 官方auth-mantine示例为主线,完整讲解如何基于 Mantine UI 与 Refine v5 从零实现一套可自定义的认证方案:包括 AuthProvider 全部方法的实现、<AuthPage>登录/注册/找回密码/更新密码四种页面、<Authenticated>路由守卫,以及 OAuth 第三方登录的接入方式。读完本文,你将掌握 Refine 认证机制的底层调用链,并能在自己的管理后台中直接落地这套方案。
一、认证在 Refine 中的工作方式
Refine 的认证能力完全由AuthProvider(认证提供器)承载。它是一个包含若干认证方法的普通对象,通过authProvider属性传给<Refine>组件后,这些方法会被 Refine 内部的认证 Hook(如useLogin、useIsAuthenticated、useGetIdentity等)自动消费:
import { Refine } from "@refinedev/core"; const App = () => { return <Refine authProvider={authProvider} />; };两点关键认知(来自 AuthProvider 官方文档):
- AuthProvider不是必需的。不传时应用没有认证能力,所有认证 Hook 与组件均不可用;
- 所有方法期望返回Promise,因此天然支持异步逻辑(请求后端、读写 localStorage、刷新 Token 等),也可以接入 Auth0、Okta 等任意第三方认证服务,或完全自定义。
从 认证接口定义 的源码结构看,AuthProvider 分为必需方法(login、check、logout、onError)与可选方法(register、forgotPassword、updatePassword、getPermissions、getIdentity)两类。下面我们结合auth-mantine示例的完整实现逐一展开。
二、示例项目结构一览
示例位于 examples/auth-mantine,依赖组合为:@refinedev/core、@refinedev/mantine、@refinedev/react-router、@refinedev/simple-rest,UI 层使用@mantine/core与@mantine/form、@mantine/notifications(见 package.json)。核心文件:
| 文件 | 职责 |
|---|---|
| examples/auth-mantine/src/App.tsx | AuthProvider 实现 + 路由 + 认证页面装配 |
| examples/auth-mantine/src/pages/posts/list.tsx | 受保护的资源页面 |
| examples/auth-mantine/src/components/header/index.tsx | 顶部栏展示当前用户身份 |
| cypress/e2e/auth-mantine/all.cy.ts | 认证全流程端到端测试 |
三、完整 AuthProvider 实现(Mock 凭据版)
示例在 examples/auth-mantine/src/App.tsx 中定义了一组模拟凭据,用于在没有真实后端的情况下演示完整认证闭环:
const authCredentials = { email: "demo@refine.dev", password: "demodemo", };下面按方法逐一剖析。
3.1 login:登录与第三方 OAuth 跳转
login: async ({ providerName, email }) => { if (providerName === "google") { window.location.href = "https://accounts.google.com/o/oauth2/v2/auth"; return { success: true }; } if (providerName === "github") { window.location.href = "https://github.com/login/oauth/authorize"; return { success: true }; } if (email === authCredentials.email) { localStorage.setItem("email", email); return { success: true, redirectTo: "/" }; } return { success: false, error: { message: "Login failed", name: "Invalid email or password", }, }; },要点解析:
login的入参来自useLogin触发 mutation 时传入的参数(示例中即providerName与email),Refine 对参数类型不做任何约束,可自由定义;- OAuth 分支:当用户点击"Sign in with Google / GitHub"时,AuthProvider 直接通过
window.location.href跳转到 OAuth 授权端点,返回success: true交由授权服务器接管后续流程; - 凭据校验分支:校验通过后把用户信息写入
localStorage作为登录态,并通过redirectTo: "/"指定登录成功后的跳转路径; - 失败分支:返回
success: false与error对象(含message与name),Refine 会自动弹出错误通知——示例中的文案在测试里被断言为包含/invalid email/i(见 cypress/e2e/auth-mantine/all.cy.ts)。
login的返回类型为AuthActionResponse:
type AuthActionResponse = { success: boolean; redirectTo?: string; error?: Error; [key: string]: unknown; };3.2 check:登录态校验
check: async () => localStorage.getItem("email") ? { authenticated: true } : { authenticated: false, error: { message: "Check failed", name: "Not authenticated" }, logout: true, redirectTo: "/login", },check是 Refine 判断"当前用户是否已认证"的入口,返回类型为CheckResponse(authenticated/redirectTo/logout/error)。用户导航到受保护页面时 Refine 会内部调用它:
- 有
email键 →authenticated: true,放行; - 无 →
authenticated: false并携带redirectTo: "/login"与logout: true,此时路由会带上to参数,登录成功后自动跳回原页面(详见"常见陷阱"一节)。
3.3 logout / onError:登出与错误兜底
logout: async () => { localStorage.removeItem("email"); return { success: true, redirectTo: "/login" }; }, onError: async (error) => { if (error.response?.status === 401) { return { logout: true }; } return { error }; },logout清除登录态并跳转/login;onError由useOnError消费,用于统一处理 API 错误:当后端返回401时返回logout: true,Refine 会调用logout自动登出——这是 JWT 过期场景的标准兜底做法。官方文档中给出的通用版本还会把403一并纳入,并支持redirectTo指定登出后去向(见 auth-provider/index.md)。
3.4 register / forgotPassword / updatePassword:完整认证闭环
register: async (params) => { if (params.email === authCredentials.email && params.password) { localStorage.setItem("email", params.email); return { success: true, redirectTo: "/" }; } return { success: false, error: { message: "Register failed", name: "Invalid email or password" }, }; }, forgotPassword: async (params) => { if (params.email === authCredentials.email) { // 在这里发送包含重置链接的邮件 return { success: true }; } return { success: false, error: { message: "Forgot password failed", name: "Invalid email" }, }; }, updatePassword: async (params) => { if (params.password === authCredentials.password) { // 在这里更新密码 return { success: true }; } return { success: false, error: { message: "Update password failed", name: "Invalid password" }, }; },三个可选方法与login返回相同的AuthActionResponse类型,分别由useRegister、useForgotPassword、useUpdatePasswordHook 消费。注册成功后同样写入localStorage并跳转首页,形成"注册 → 登录 → 修改密码"的完整用户旅程。
3.5 getPermissions / getIdentity:权限与身份
getPermissions: async () => ["admin"], getIdentity: async () => ({ id: 1, name: "Jane Doe", avatar: "https://unsplash.com/photos/IWLOvomUmWU/download?force=true&w=640", }),getPermissions返回用户权限数组,可配合usePermissions做轻量级授权判断;官方文档提示:复杂授权场景建议改用 access control provider(见 authorization 文档);getIdentity返回当前用户身份对象,供useGetIdentity消费。示例顶部栏正是用它渲染用户名与头像(见 components/header/index.tsx):
import { useGetIdentity } from "@refinedev/core"; const { data: user } = useGetIdentity<IUser>(); {(user?.name || user?.avatar) && ( <Group spacing="xs"> {user?.name && <Title order={6}>{user?.name}</Title>} <Avatar src={user?.avatar} alt={user?.name} radius="xl" /> </Group> )}四、<AuthPage>:四种认证页面一键装配
<AuthPage>是 Refine 内置的认证页面组件(源码位于 packages/core/src/components/pages/auth/index.tsx),通过type属性切换四种形态:login、register、forgotPassword、updatePassword。在 Mantine 版本中它直接产出适配 Mantine 主题的表单。
示例中的登录页同时接入了 Google / GitHub 两个 OAuth 提供方:
<Route path="/login" element={ <AuthPage type="login" formProps={{ initialValues: { ...authCredentials, // 预填 demo@refine.dev / demodemo,方便演示 }, }} providers={[ { name: "google", label: "Sign in with Google", icon: <IconBrandGoogle />, }, { name: "github", label: "Sign in with GitHub", icon: <IconBrandGithub />, }, ]} /> } />要点:
providers数组中的每一项都会渲染为一个第三方登录按钮,其name会作为providerName传入login方法——这正是上一节 OAuth 分支的触发来源;- 注册页同样传入
providers(见 App.tsx 的 /register 路由),而/forgot-password与/update-password仅使用默认表单。
五、<Authenticated>路由守卫与页面布局
认证状态如何转化为"路由拦截"?示例通过<Authenticated>组件包裹路由分组实现,它内部基于check方法的结果决定渲染子内容还是fallback:
<Route element={ <Authenticated key="authenticated-routes" fallback={<CatchAllNavigate to="/login" />} > <ThemedLayout> <Outlet /> </ThemedLayout> </Authenticated> } > <Route index element={<NavigateToResource resource="posts" />} /> <Route path="/posts"> <Route index element={<PostList />} /> <Route path="create" element={<PostCreate />} /> <Route path="edit/:id" element={<PostEdit />} /> <Route path="show/:id" element={<PostShow />} /> </Route> </Route>这段代码是"受保护区域"的典型写法:
- 未登录访问
/posts等受保护路由 →<CatchAllNavigate to="/login" />将用户重定向到登录页; - 已登录访问
/login、/register等认证页 → 反向拦截,<NavigateToResource resource="posts" />把用户送回首资源页; - 兜底路由(404)同样放在
<Authenticated>包裹的<ThemedLayout>内,未登录用户不会看到错误页而是先去登录(见 App.tsx 末尾的 catch-all 路由)。
此外示例还启用了两个实用选项:
options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }}分别用于 URL 与筛选/分页状态同步、以及表单未保存离开时的提醒(配合<UnsavedChangesNotifier />使用)。
六、从源码看认证 Hook 的调用链
结合 AuthProvider 官方文档 与示例实现,各方法的消费端对应关系如下:
| AuthProvider 方法 | 消费 Hook | 典型使用场景 |
|---|---|---|
login | useLogin | 登录表单提交 |
check | useIsAuthenticated | 路由守卫、页面加载时的认证判断 |
logout | useLogout | 顶部栏退出按钮 |
onError | useOnError | 全局 API 错误处理 |
register | useRegister | 注册表单提交 |
forgotPassword | useForgotPassword | 忘记密码页 |
updatePassword | useUpdatePassword | 重置密码页 |
getPermissions | usePermissions | 按钮级权限控制 |
getIdentity | useGetIdentity | 顶部栏展示用户信息 |
需要特别强调的是redirectTo的语义:登录、注册、登出方法都可返回redirectTo指定跳转路径;若不需要跳转,返回redirectTo: undefined即可;错误信息则统一通过error: { name, message }定制,Refine 会在success: false时自动弹出通知(详见 auth-provider/index.md 的 FAQ 部分)。
七、端到端验证:Cypress 测试覆盖的行为契约
示例的认证逻辑并非孤立的演示代码,cypress/e2e/auth-mantine/all.cy.ts 中的端到端测试把关键行为固化为契约,值得逐条对照实现:
登录与重定向:
it("should redirect to /login?to= if user not authenticated", () => { cy.visit("/test-route"); cy.get(".mantine-Title-root").contains(/sign in to your account/i); cy.location("search").should("contains", "to=%2Ftest"); cy.location("pathname").should("eq", "/login"); });登出:
it("should logout", () => { login(); cy.get("button").contains(/logout/i).click(); cy.location("pathname").should("eq", "/login"); });身份展示:
it("should render getIdentity response on header", () => { login(); cy.get(".mantine-Text-root").contains(/jane doe/i); cy.get(".mantine-Avatar-image").should("have.attr", "src"); });测试共覆盖:登录成功、错误凭据提示、未登录重定向携带to参数、登录后回到原访问页、注册成功/失败、忘记密码失败、更新密码失败、登出、身份渲染九大场景。这意味着你在 examples/auth-mantine 中看到的每个方法都有对应的行为断言,可直接作为自定义认证逻辑的验收标准。
八、常见陷阱与实战建议
1.to参数与登录后回跳:未登录访问受保护页面时,Refine 会把原目标路径编码到/login?to=...;login成功后自动跳回。自定义login实现时不要破坏这个链路,也不要在redirectTo里硬编码路径覆盖回跳逻辑。
2. 401 兜底必须接入:生产环境接入 JWT 时,务必实现onError并对 401/403 返回{ logout: true },否则 Token 过期后页面会停留在"假登录"状态。
3. OAuth 与本地态分离:示例中 OAuth 分支直接window.location.href跳转,登录态最终仍以localStorage为准。真实项目中应在 OAuth 回调页完成 code 换取 token,再写入存储,check保持一致。
4. AuthProvider 是纯接口:所有方法都是 async 函数,入参自由、返回类型固定。后端 API 形态(REST、GraphQL)不会影响本方案,只需在方法内部替换为真实请求即可。
5. 权限与身份尽量走 Hook:不要在组件里直接读localStorage,统一通过useGetIdentity、usePermissions获取,便于未来切换到真实服务端鉴权时只改 AuthProvider。
九、运行示例
示例使用 Vite + Refine CLI 构建(package.json):
cd examples/auth-mantine pnpm install pnpm dev # 开发模式,Node >= 20 pnpm build # tsc && refine build启动后访问/,使用预填的demo@refine.dev / demodemo即可体验完整认证流程;点击 Google / GitHub 按钮则触发 OAuth 跳转。示例与文档相互印证:完整的 AuthProvider 方法说明见 AuthProvider 官方文档,认证页面组件见 AuthPage 文档,各认证 Hook 的详细用法位于 authentication/hooks 目录。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考