Table of Contents
使用 Supabase 和 GitHub 授权实现用户认证
在现代 Web 应用开发中,用户认证是非常基础且重要的功能。传统的用户名密码认证方式虽然常见,但实现起来需要考虑密码加密、安全存储、找回密码等诸多细节。而社交登录(如 GitHub、Google 等)可以让用户更便捷地登录应用,同时简化开发流程。
本文将详细介绍如何在 Next.js 应用中使用 Supabase 和 GitHub OAuth 实现用户认证功能。Supabase 是一个开源的 Firebase 替代品,提供数据库、认证、存储等完整的后端服务。
前置准备
步骤一:创建 Supabase 项目
- 登录 Supabase 控制台
- 点击 "New Project"
- 填写项目名称、设置密码,选择区域(建议选择距离用户最近的区域)
- 点击 "Create new project",等待项目创建完成
步骤二:配置 GitHub OAuth
在 GitHub 创建 OAuth 应用
- 登录 GitHub,进入 Developer Settings
- 选择 "OAuth Apps" -> "New OAuth App"
- 填写应用信息:
- Application name:你的应用名称
- Homepage URL:你的应用 URL(开发环境可填
http://localhost:3000
) - Authorization callback URL:填写 Supabase 回调 URL
- 格式:
https://<YOUR_SUPABASE_PROJECT>.supabase.co/auth/v1/callback
- 请将
<YOUR_SUPABASE_PROJECT>
替换为你的 Supabase 项目 ID
- 格式:
- 点击 "Register application"
- 创建成功后,点击 "Generate a new client secret" 生成密钥
在 Supabase 配置 GitHub 提供商
- 在 Supabase 控制台,进入项目
- 左侧菜单选择 "Authentication" -> "Providers"
- 找到 GitHub,点击启用
- 填入上一步从 GitHub 获取的 Client ID 和 Client Secret
- 点击 "Save" 保存设置
步骤三:在 Next.js 项目中配置 Supabase
安装必要的依赖
npm install @supabase/supabase-js
# 或使用 yarn
yarn add @supabase/supabase-js
创建 Supabase 客户端
在你的项目中创建一个 lib/supabase.ts
文件:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
配置环境变量
在项目根目录创建 .env.local
文件:
NEXT_PUBLIC_SUPABASE_URL=https://你的项目ID.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=你的匿名密钥
注意:这些值可以从 Supabase 控制台的 "Settings" -> "API" 页面获取。
步骤四:实现用户认证功能
创建自定义 Hook 管理用户状态
创建 hooks/useUser.ts
文件:
'use client'
import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabase'
import { User } from '@supabase/supabase-js'
export function useUser() {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
// 获取当前用户信息
const fetchUser = async () => {
try {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
} catch (error) {
console.error('获取用户信息错误:', error)
} finally {
setIsLoading(false)
}
}
// 监听认证状态变化
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
setIsLoading(false)
})
fetchUser()
// 清理订阅
return () => {
subscription.unsubscribe()
}
}, [])
// GitHub 登录方法
const signIn = async () => {
setIsLoading(true)
const { error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/dashboard`, // 登录成功后重定向的页面
},
})
if (error) {
console.error('登录错误:', error)
setIsLoading(false)
}
}
// 登出方法
const signOut = async () => {
setIsLoading(true)
const { error } = await supabase.auth.signOut()
if (error) {
console.error('登出错误:', error)
}
setIsLoading(false)
}
return { user, isLoading, signIn, signOut }
}
创建登录按钮组件
创建 components/LoginButton.tsx
组件:
'use client'
import { useUser } from '@/hooks/useUser'
export default function LoginButton() {
const { user, isLoading, signIn, signOut } = useUser()
return (
<div>
{isLoading ? (
<button
disabled
className="rounded-md bg-gray-200 px-4 py-2 text-sm font-medium text-gray-400"
>
加载中...
</button>
) : user ? (
<div className="flex items-center gap-4">
<span className="text-sm text-gray-700">
欢迎,{user.user_metadata.name || user.email}
</span>
<button
onClick={signOut}
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
退出登录
</button>
</div>
) : (
<button
onClick={signIn}
className="flex items-center gap-2 rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="currentColor"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
使用 GitHub 登录
</button>
)}
</div>
)
}
在页面中使用登录按钮
现在,你可以在任何需要的页面中导入和使用 LoginButton
组件:
import LoginButton from '@/components/LoginButton'
export default function HomePage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h1 className="mb-8 text-3xl font-bold">我的应用</h1>
<LoginButton />
</div>
)
}
步骤五:创建受保护的路由
对于需要登录才能访问的页面,可以创建一个简单的保护组件:
'use client'
import { useEffect } from 'react'
import { useUser } from '@/hooks/useUser'
import { useRouter } from 'next/navigation'
export default function ProtectedRoute({
children,
}: {
children: React.ReactNode
}) {
const { user, isLoading } = useUser()
const router = useRouter()
useEffect(() => {
if (!isLoading && !user) {
router.push('/login')
}
}, [isLoading, user, router])
if (isLoading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-blue-500" />
</div>
)
}
if (!user) {
return null
}
return <>{children}</>
}
然后在需要保护的页面中使用:
'use client'
import ProtectedRoute from '@/components/ProtectedRoute'
export default function DashboardPage() {
return (
<ProtectedRoute>
<div className="p-8">
<h1 className="text-2xl font-bold">控制面板</h1>
<p className="mt-4">这是一个受保护的页面,只有登录用户才能访问。</p>
</div>
</ProtectedRoute>
)
}
步骤六:设置数据库表和行级安全策略(RLS)
如果你需要存储用户相关的数据,比如用户个人资料或者用户创建的内容,应该设置相应的数据库表和安全策略。
以下是一个创建用户个人资料表的 SQL 示例:
-- 创建用户个人资料表
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE,
avatar_url TEXT,
updated_at TIMESTAMP WITH TIME ZONE
);
-- 启用行级安全策略
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- 创建安全策略
CREATE POLICY "公开可读取用户资料"
ON profiles
FOR SELECT
USING (true);
CREATE POLICY "用户可以更新自己的资料"
ON profiles
FOR UPDATE
USING (auth.uid() = id);
-- 创建触发器,当用户注册时自动创建资料
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, avatar_url, updated_at)
VALUES (NEW.id, NEW.raw_user_meta_data->>'name', NEW.raw_user_meta_data->>'avatar_url', NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
你可以在 Supabase 控制台的 "SQL Editor" 中执行此 SQL 代码。
总结
通过本文的步骤,你已经成功实现了使用 Supabase 和 GitHub OAuth 的用户认证功能。这种方式有以下优势:
- 简化开发流程:不需要自己实现复杂的认证逻辑
- 提高安全性:OAuth 由专业的第三方服务处理
- 改善用户体验:用户无需创建新账号,使用已有的 GitHub 账号即可登录
- 获取额外信息:可以获取用户在 GitHub 上的信息(如头像、用户名等)
此外,Supabase 还支持多种其他认证方式,如电子邮件/密码、谷歌、Facebook 等,你可以根据需求添加更多的登录选项。
最后,别忘了在生产环境部署前,更新相关的回调 URL 和其他设置。对于生产环境,还应该考虑添加更多的安全措施,如设置合适的 CORS 策略和 API 限制。