- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
本篇技术指南以 Redwood 项目中的官方实战手册(docs/versioned_docs/version-6.x/how-to/supabase-auth.md)为核心,完整讲解如何将 Supabase 认证(Auth)接入 Redwood 应用:从一条 CLI 命令完成配置、在管理控制台获取密钥,到亲手实现注册(Sign Up)页、登录(Sign In)页、登出(Sign Out)按钮,以及根据认证状态自动切换的导航链接。读完后,你将掌握 Redwood Auth 抽象层与 Supabase GoTrueJS 客户端之间的协作关系,并能独立为任意 Redwood 页面接入完整的邮箱密码认证闭环。
为什么选择 Supabase Auth
这篇 How-to 本质上可视为 Redwood 官方 GoTrue Auth 实战手册 的 Supabase 移植版。原因很简单:Supabase 的认证服务建立在 Netlify GoTrue 这一开源基础之上——它是一个专门处理用户注册与认证的 API 服务。当你理顺这条技术脉络后,GoTrue 系的实战手册就变成了攀登认证学习曲线的现成阶梯。
对 Redwood 来说,最终使用的是 Redwood Auth 包装层,它提供了一层抽象与统一、简洁的 API 风格。不过,先直接与 Supabase Client 打交道一两个小时(例如跟随 Supabase 的 React Quick Start 走一遍),会对理解 Redwood 包装层所隐藏的细节大有帮助。
前置准备
开始之前,请依次完成以下步骤:
- 创建 Redwood 应用
- 注册 Supabase 账户并创建组织与项目
- 完成 Supabase 的 React Quick Start(先直连 Supabase Client,建立直观感受)
- 完成 Supabase 的 Redwood Quick Start(观察与纯 React 用法的差异)
- 启动开发服务器:
yarn redwood dev
认证术语梳理
初次接触时,认证领域的名词容易让人头晕。这里帮你理清一条主线:Redwood 的 Supabase 认证客户端包装了 Supabase 的 GoTrueJS 客户端,而后者是 Netlify GoTrueJS 客户端的分支(它与 Netlify Identity 不同);dbAuth 则是 Redwood 完全独立的一种认证方案。本教程暂不涉及 RBAC(基于角色的访问控制)。
安装与初始化:一条命令搞定配置
有了 Supabase Quick Start 的知识储备,回到我们创建好的 Redwood 应用。安装所需包并生成 Redwood Auth 的样板代码,只需一条 CLI 命令:
yarn redwood setup auth supabase指定supabase作为 provider 后,Redwood 会自动向应用写入必要的 Supabase 配置。在 CLI 源码中可以看到,该命令在 packages/cli/src/commands/setup/auth/auth.js 中注册为setup auth <provider>的子命令,执行时会动态加载@redwoodjs/auth-supabase-setup包(若未安装则自动以与框架一致的版本yarn add -D安装),再调用其 handler 完成文件写入与代码修改。
打开web/src/App.[js/tsx]检查,你应该看到:
import { AuthProvider } from '@redwoodjs/auth' import { createClient } from '@supabase/supabase-js' import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import FatalErrorPage from 'src/pages/FatalErrorPage' import Routes from 'src/Routes' import './index.css' const supabaseClient = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) const App = () => ( <FatalErrorBoundary page={FatalErrorPage}> <RedwoodProvider titleTemplate="%PageTitle | %AppTitle"> <AuthProvider client={supabaseClient} type="supabase"> <RedwoodApolloProvider> <Routes /> </RedwoodApolloProvider> </AuthProvider> </RedwoodProvider> </FatalErrorBoundary> ) export default App核心动作是第 12 行的<AuthProvider client={supabaseClient} type="supabase">:把用SUPABASE_URL和SUPABASE_KEY创建的 Supabase 客户端实例注入到整个组件树中,供所有子组件通过useAuth钩子访问。
在较新版本的 Redwood 中(对应仓库当前的 auth.ts.template),初始化方式稍有演进:setup 会在web/src/auth.ts生成如下代码,把客户端创建与AuthProvider/useAuth的装配封装在createAuth中,页面侧则统一从'src/auth'导入:
import { createClient } from '@supabase/supabase-js' import { createAuth } from '@redwoodjs/auth-supabase-web' const supabaseClient = createClient( process.env.SUPABASE_URL || '', process.env.SUPABASE_KEY || '' ) export const { AuthProvider, useAuth } = createAuth(supabaseClient)两种写法的本质相同:最终都通过createAuth把 Supabase 客户端包装成 Redwood Auth 统一接口(login、logout、signup、getToken等),实现见 packages/auth-providers/supabase/web/src/supabase.ts。
环境变量配置
现在把 Supabase 的 URL、公共 API Key 和 JWT 密钥(SUPABASE_URL、SUPABASE_KEY、SUPABASE_JWT_SECRET)写入.env文件。这些信息可在 Supabase 管理控制台获取:
- Project URL 与 API key(anon / public):新建项目后控制台展示的项目 URL 和 API Key;若页面刷新错过,可在「Connecting to your new project」区域找到。
- JWT Secret:位于左侧导航底部的「Project Settings(齿轮图标)」→「API」→ 向下滚动至「JWT Settings」下的「JWT Secret」。
.env示例如下:
# .env (in your root project directory) SUPABASE_URL=https://replacewithyoursupabaseurl.supabase.co SUPABASE_KEY=eyJhb_replace_VCJ9.eyJy_with_your_wfQ.0Abb_anon_key_teLJs SUPABASE_JWT_SECRET=eyJh_replace_CJ9.eyJy_with_your_NTQwOTB9.MGNZN_JWT_secret_JgErqxj4此外,还要在redwood.toml中把SUPABASE_URL和SUPABASE_KEY加入 web 侧可见的环境变量白名单(JWT Secret 仅用于 api 侧校验,不需要暴露给浏览器):
[web] # ... includeEnvironmentVariables = ["SUPABASE_URL", "SUPABASE_KEY"]至此配置基本完成。还有一个可选项:若注册流程不需要邮箱确认,可在 Supabase 管理控制台Authentication > Settings中关闭「Enable email confirmations」。
源码视角:Redwood Auth 如何包装 Supabase
理解底层实现有助于排查问题。在 supabase.ts 中,createAuthImplementation返回的 auth 实现把 Supabase SDK 的多种登录方式统一收敛到login方法,通过credentials.authMethod分发到不同底层调用:
password→supabaseClient.auth.signInWithPassword(credentials):邮箱/手机号 + 密码登录oauth→supabaseClient.auth.signInWithOAuth(credentials):GitHub、Google 等第三方登录otp→supabaseClient.auth.signInWithOtp(credentials):魔法链接或一次性密码(支持邮箱与手机)sso→supabaseClient.auth.signInWithSSO(credentials):企业级单点登录(实验性)id_token→supabaseClient.auth.signInWithIdToken(credentials):用受支持厂商签发的 ID Token 登录(实验性)- 其他情况返回携带
AuthError('Unsupported authentication method')的结果
signup直接调用supabaseClient.auth.signUp(credentials),且若返回结果包含session(即服务端开启了自动确认),会同步写入auth-providercookie;logout调用supabaseClient.auth.signOut();getToken与getUserMetadata都基于supabaseClient.auth.getSession()返回会话与用户元数据;restoreAuthState则在 OAuth 或魔法链接回调重定向回站点时,通过refreshSession()恢复会话并清理 URL 中的access_token、refresh_token等认证参数。
setup 同时会生成 api 侧的 api/src/lib/auth.ts:getCurrentUser解析并解码 JWT 返回用户信息与可选角色,requireAuth/hasRole/isAuthenticated供 service 层做登录与角色校验(未登录抛AuthenticationError,角色不符抛ForbiddenError)。注意getCurrentUser的返回值会暴露给 web 端,务必只返回安全的字段。
注册页面(Sign Up)
先生成注册页面:
yarn redwood generate page signup这会向路由文件添加一条注册路由,并创建SignupPage组件。在web/src/pages/SignupPage/SignupPage.[js/tsx]中导入 Redwood Form 组件,做一个最基础的表单:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' const SignupPage = () => { return ( <> <h1>Sign Up</h1> <Form> <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } export default SignupPage想要更精致的表单样式,可参考 Redwood Form 文档与教程的表单章节。这里聚焦功能本身——为表单添加onSubmit:
// ... const SignupPage = () => { const onSubmit = (data) => { // do something here } return ( <> <h1>Sign Up</h1> <Form onSubmit={onSubmit}> <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } //...要做的事是——注册。为此需要与<AuthProvider />及其内部包装的 Supabase GoTrue-JS 客户端通信,这就要用到useAuth钩子。导入useAuth并从中解构出client(即我们传入的 Supabase GoTrue-JS 实例,可调用其全部方法):
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' const SignupPage = () => { const { client } = useAuth() const onSubmit = (data) => { // do something here } return ( <> <h1>Sign Up</h1> <Form onSubmit={onSubmit}> <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } export default SignupPage在onSubmit中通过client.auth.signUp()传入表单捕获的email与password来创建新用户:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' const SignupPage = () => { const { client } = useAuth() const onSubmit = async (data) => { try { const response = await client.auth.signUp({ email: data.email, password: data.password }) console.log('response: ', response) } catch(error) { console.log('error: ', error) } } return ( <> <h1>Sign Up</h1> <Form onSubmit={onSubmit}> <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } export default SignupPage目前注册可用,但仅把响应打印到控制台并不实用。接下来用React.useState()管理错误状态,条件渲染错误消息,并在每次提交前用setError(null)重置:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' const SignupPage = () => { const { client } = useAuth() const [error, setError] = React.useState(null) const onSubmit = async (data) => { setError(null) try { const response = await client.auth.signUp({ email: data.email, password: data.password }) console.log('response: ', response) response?.error?.message && setError(response.error.message) } catch(error) { setError(error.message) } } return ( <> <h1>Sign Up</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } export default SignupPage错误有两种返回方式:
- promise 正常兑现(fulfillment)时,错误位于返回对象内的
error属性中;- promise 被拒绝(rejection)时,错误通过 promise 本身抛出(由
catch块捕获处理)。
再处理成功提交的情况:若关闭了邮箱确认,注册成功即等同于登录成功,此时应把用户重定向回应用首页。先生成首页(若还没有):
yarn redwood generate page home /然后从 Redwood Router 导入routes与navigate,在注册成功后跳转:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' import { routes, navigate } from '@redwoodjs/router' const SignupPage = () => { const { client } = useAuth() const [error, setError] = React.useState(null) const onSubmit = async (data) => { setError(null) try { const response = await client.auth.signUp({ email: data.email, password: data.password }) response?.error?.message ? setError(response.error.message) : navigate(routes.home()) } catch(error) { setError(error.message) } } return ( <> <h1>Sign Up</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign Up</Submit> </Form> </> ) } export default SignupPage注册页完成:注册表单、错误展示、成功后跳转首页一应俱全。
登录页面(Sign In)
先生成登录页:
yarn redwood generate page signin添加含email、password字段的基础表单、错误提示与空的onSubmit:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' const SigninPage = () => { const [error, setError] = React.useState(null) const onSubmit = (data) => { // do sign in here } return ( <> <h1>Sign In</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign In</Submit> </Form> </> ) } export default SigninPage导入useAuth并解构出logIn:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' const SigninPage = () => { const { logIn } = useAuth() const [error, setError] = React.useState(null) const onSubmit = (data) => { setError(null) // do sign in here } return ( <> <h1>Sign In</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign In</Submit> </Form> </> ) } export default SigninPage与注册不同,这次直接调用 Redwood Auth 的logIn函数(而非经由client),向其传入包含 email 与 password 的对象:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' const SigninPage = () => { const { logIn } = useAuth() const [error, setError] = React.useState(null) const onSubmit = async (data) => { setError(null) try { const response = await logIn({ email: data.email, password: data.password }) // do something } catch(error) { setError(error.message) } } return ( <> <h1>Sign In</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign In</Submit> </Form> </> ) } export default SigninPage登录成功后重定向回首页。从@redwoodjs/router导入navigate与routes,在await logIn之后使用:
import { Form, TextField, PasswordField, Submit } from '@redwoodjs/forms' import { useAuth } from '@redwoodjs/auth' import { navigate, routes } from '@redwoodjs/router' const SigninPage = () => { const { logIn } = useAuth() const [error, setError] = React.useState(null) const onSubmit = async (data) => { setError(null) try { const response = await logIn({ email: data.email, password: data.password }) response?.error?.message ? setError(response.error.message) : navigate(routes.home()) } catch(error) { setError(error.message) } } return ( <> <h1>Sign In</h1> <Form onSubmit={onSubmit}> {error && <p>{error}</p>} <TextField name="email" placeholder="email" /> <PasswordField name="password" placeholder="password" /> <Submit>Sign In</Submit> </Form> </> ) } export default SigninPage手册剩余部分与 Netlify GoTrue Auth 版本一致,这正体现了 Redwood Auth 包装层的乐趣:特定认证方案相关的代码只集中在上面几个位置,其余部分可统一使用通用的 Redwood Auth 函数。
在 Redwood 中,logIn是统一入口:除密码登录外,它同样支持 OAuth、OTP、ID Token 与 SSO 等方式,只需传入不同的authMethod(对应源码中的credentials.authMethod分发逻辑)。例如 GitHub OAuth 登录可写作logIn({ authMethod: 'oauth', provider: 'github' })。
登出按钮(Sign Out)
登出是最容易实现的——只需调用useAuth的logOut方法。先生成组件:
yarn redwood generate component signoutBtn在web/src/components/SignoutBtn/SignoutBtn.js中渲染按钮并添加点击处理器:
const SignoutBtn = () => { const onClick = () => { // do sign out here. } return <button onClick={() => onClick()}>Sign Out</button> } export default SignoutBtn导入useAuth,解构logOut并在onClick中调用:
import { useAuth } from '@redwoodjs/auth' const SignoutBtn = () => { const { logOut } = useAuth() const onClick = () => { logOut() } return <button onClick={() => onClick()}>Sign Out</button> } export default SignoutBtn这已经能工作,但用户可能正处在受保护的页面,登出后应将其导航离开当前页:
import { useAuth } from '@redwoodjs/auth' import { navigate, routes } from '@redwoodjs/router' const SignoutBtn = () => { const { logOut } = useAuth() const onClick = async () => { await logOut() navigate(routes.home()) } return <button onClick={() => onClick()}>Sign Out</button> } export default SignoutBtn在底层,logOut调用supabaseClient.auth.signOut():清除浏览器会话与 localStorage,并触发SIGNED_OUT事件(见 supabase.ts 中logout的实现)。目前这个按钮还没有渲染到任何地方——下一步就把它接进根据认证状态切换的导航。
认证状态导航(Auth Links)
实现一段根据用户认证状态条件渲染的导航:
- 用户未登录时:显示Sign Up与Sign In
- 用户已登录时:显示Log Out
先生成导航组件:
yarn redwood generate component navigation在web/src/components/Navigation/Navigation.js中导入 Router 的Link组件与routes对象,以及用于订阅认证状态的useAuth:
import { Link, routes } from '@redwoodjs/router' import { useAuth } from '@redwoodjs/auth' const Navigation = () => { return <nav></nav> } export default Navigation从useAuth解构isAuthenticated并在条件语句中使用:
import { Link, routes } from '@redwoodjs/router' import { useAuth } from '@redwoodjs/auth' const Navigation = () => { const { isAuthenticated } = useAuth() return ( <nav> {isAuthenticated ? ( // signed in - show the Sign Out button ) : ( // signed out - show the Sign Up and Sign In links )} </nav> ) } export default NavigationRedwood Auth 基于 React 的 Context API 管理与广播认证状态,因此可以放心:只要组件位于<AuthProvider />之下,isAuthenticated就会始终是最新的——即使认证状态在组件树中的其他组件里发生变化,React 也会自动重渲染出正确的组件。
把登出按钮以及登录、注册链接放进条件块的对应分支:
import { Link, routes } from '@redwoodjs/router' import { useAuth } from '@redwoodjs/auth' import SignoutBtn from 'src/components/SignoutBtn/SignoutBtn' const Navigation = () => { const { isAuthenticated } = useAuth() return ( <nav> {isAuthenticated ? ( <SignoutBtn /> ) : ( <> <Link to={routes.signup()}>Sign Up</Link> <Link to={routes.signin()}>Sign In</Link> </> )} </nav> ) } export default Navigation导航组件已经可用,还需要把它渲染到页面中。先生成一个名为 GlobalLayout 的布局:
yarn redwood generate layout global在生成的web/src/layouts/GlobalLayout/GlobalLayout中导入并渲染导航组件:
import Navigation from 'src/components/Navigation/Navigation' const GlobalLayout = ({ children }) => { return ( <> <header> <Navigation /> </header> <main>{children}</main> </> ) } export default GlobalLayout最后,用 Router 的Set组件把每个页面高效地包裹进GlobalLayout,更新web/src/Routes.[js/tsx]:
import { Router, Route, Set } from '@redwoodjs/router' import GlobalLayout from 'src/layouts/GlobalLayout/GlobalLayout' const Routes = () => { return ( <Router> <Set wrap={GlobalLayout}> <Route path="/" page={HomePage} name="home" /> <Route path="/signup" page={SignUpPage} name="signup" /> <Route path="/signin" page={SignInPage} name="signin" /> </Set> <Route notfound page={NotFoundPage} /> </Router> ) } export default Routes现在导航会根据认证状态渲染正确的链接与按钮:登录后看到Sign Out按钮,登出后看到Sign Up与Sign In链接。
测试与验证:认证实现的正确性保障
仓库为 Supabase 认证实现提供了完整的单元测试,见 packages/auth-providers/supabase/web/src/tests/supabase.test.tsx。这些用例覆盖了本手册涉及的全部核心场景,可作为自测清单:
- 登录前
isAuthenticated为false; - 邮箱 + 密码注册(含附加元数据、重定向 URL 两种变体)后认证状态为
true,且currentUser.email与userMetadata正确; - 密码登录后
isAuthenticated为true,登出后恢复为false; hasRole支持从 Supabase 的app_metadata.roles读取角色,也支持通过useHasRole/useCurrentUser自定义钩子覆写默认行为;- OAuth、OTP、ID Token、SSO 四种
logIn方式均能正确建立认证状态。
此外,supabase.middleware.test.tsx 验证了流式 SSR 场景下中间件认证(middleware auth)的行为。
收尾与延伸
至此,我们已经完成了:用 Redwood Auth 配置 Supabase GoTrue Auth,创建注册页、登录页与登出按钮,并把认证链接接入布局。配置、注册、登录、登出、状态化导航的完整闭环已经跑通。
后续深入时值得留意的特性(未来的实战手册可以逐个深挖):
- 认证状态变更监听:可通过 Supabase 客户端的
onAuthStateChange事件监听SIGNED_IN、SIGNED_OUT、TOKEN_REFRESHED、USER_UPDATED、PASSWORD_RECOVERY等事件(需在useEffect中调用并记得退订)。 - 更多认证方式:
- Passwordless(输入邮箱获取魔法确认链接,对应
authMethod: 'otp') - 第三方登录(GitHub、Google 等,对应
authMethod: 'oauth') - 手机一次性密码(OTP)
- 使用刷新令牌(refresh token)登录——JWT 是认证实现的关键组成部分
- Passwordless(输入邮箱获取魔法确认链接,对应
- 受保护页面:在 api 侧通过
requireAuth保护 service,未认证用户将收到AuthenticationError。
相关参考资源:
- Redwood Supabase Auth 安装与设置文档
- 版本 6.x 的 Supabase 认证专章
- Redwood Supabase Auth 客户端实现:packages/auth-providers/supabase/web/src/supabase.ts
- Supabase 认证客户端使用与配置说明:packages/auth-providers/supabase/web/README.md
- setup 命令注册源码:packages/cli/src/commands/setup/auth/auth.js
- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
相关推荐
Redwood + Supabase Auth 实战指南:GoTrue 认证接入、注册登录与状态导航
Redwood + Supabase Auth 实战指南:GoTrue 认证接入、注册登录与状态导航 本文是一份基于 Redwood 官方文档《Supabase
后端前端Web框架开发工具Redwood 接入 Supabase Auth 完整实战:从 setup auth 到注册、登录、登出与条件导航
Redwood 接入 Supabase Auth 完整实战:从 setup auth 到注册、登录、登出与条件导航 本篇指南以 Redwood 官方 How t
后端前端Web框架开发工具Repomix 遠端 GitHub 倉庫處理指南:從 --remote 打包到遠端設定信任機制
Repomix 遠端 GitHub 倉庫處理指南:從 remote 打包到遠端設定信任機制 Repomix 可以把整個程式碼庫打包成單一、對 AI 友善的檔案,
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考