Next.js 有许多 OAuth 认证方案来实现 Github 或者 Google 登录,比较常见的有 next-auth、clerk、supabase等。Supabase提供了很多的核心服务,包括 PostgreSQL 数据库、身份验证、文件存储等。
本文将介绍如何使用 Supabase 实现 Github 登录,您将学到:
- 使用 OAuth 认证登录。
- 使用 Github 注册自动创建用户表数据。
- 用户数据缓存(zustand)。
- 路由守卫。
在继续开始前,您需要具备以下的基本知识:
- Node.js
- npm/pnpm
- Next.js
起步
项目初始化
使用 pnpm 创建最新的 Next.js 项目。
Node.js 版本至少需要 v18.17。
PS J:\next-project>pnpm create create-next-app@latest
√ What is your project named? ... next-auth
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended)... No / Yes
√ Would you like to customize the default importalias(@/*)? ... No / Yes
dependencies:
+ next 14.0.3
+ react 18.2.0
+ react-dom 18.2.0
devDependencies:
+ @types/node 20.10.3
+ @types/react 18.2.41
+ @types/react-dom 18.2.17
+ autoprefixer 10.4.16
+ eslint 8.55.0
+ eslint-config-next 14.0.3
+ postcss 8.4.32
+ tailwindcss 3.3.5
+ typescript 5.3.2
在终端启动项目:
pnpm run dev
浏览器打开 http://localhost:3000/ 将看到:
创建 Supabase 项目
- 首先进入 supabase 创建一个账户。
- 登录成功后进入 dashboard ,点击 New project。
- 设置项目名、数据库密码以及所属地区。
开始
为了实现一个好看的页面,我这里将使用 shadcn-ui 来作为项目的 ui 组件库。
安装 shadcn-ui:
PS J:\next-project\next-auth>pnpm dlx shadcn-ui@latest init
√ Would you like to use TypeScript (recommended)? ... no / yes
√ Which style would you like to use? » New York
√ Which color would you like to use as base color? » Zinc
√ Where is your global CSS file? ... app/globals.css
√ Would you like to use CSS variables for colors? ... no / yes
√ Where is your tailwind.config.js located? ... tailwind.config.ts
√ Configure the importaliasfor components: ... @/components
√ Configure the importaliasfor utils: ... @/lib/utils
√ Are you using React Server Components? ... no / yes
√ Write configuration to components.json. Proceed? ... yes
添加 Button 按钮:
pnpm dlx shadcn-ui@latest add button
添加 Lucide 图标库:
pnpminstall lucide-react
修改
app/page.tsx
:
import { Button } from "@/components/ui/button";
import { Github } from "lucide-react";
export default function Home() {
return (
<div>
<Button className="flex items-center gap-1">
<Github size={18} />
Login
</Button>
</div>
);
}
在 Next.js 中使用 Supabase
安装 Supabase 包
pnpminstall @supabase/ssr @supabase/supabase-js
在项目根目录新建一个
.env.local
文件,SUPABASE_URL 和 SUPABASE_ANON_KEY 可以在 https://supabase.com/dashboard/project/_/settings/api 中获取。
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
在根目录新建
middleware.ts
文件。输入以下内容:
import{ createServerClient,typeCookieOptions}from'@supabase/ssr'import{ NextResponse,typeNextRequest}from'next/server'exportasyncfunctionmiddleware(request: NextRequest){let response = NextResponse.next({
request:{
headers: request.headers,},})const supabase =createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,{
cookies:{get(name:string){return request.cookies.get(name)?.value
},set(name:string, value:string, options: CookieOptions){
request.cookies.set({
name,
value,...options,})
response = NextResponse.next({
request:{
headers: request.headers,},})},remove(name:string, options: CookieOptions){
request.cookies.set({
name,
value:'',...options,})
response = NextResponse.next({
request:{
headers: request.headers,},})},},})await supabase.auth.getSession()return response
}
新建
/app/auth/callback/route.ts
文件:
import{ cookies }from"next/headers";import{ NextResponse }from"next/server";import{typeCookieOptions, createServerClient }from"@supabase/ssr";exportasyncfunctionGET(request: Request){const{ searchParams, origin }=newURL(request.url);const code = searchParams.get("code");// if "next" is in param, use it as the redirect URLconst next = searchParams.get("next")??"/";if(code){const cookieStore =cookies();const supabase =createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,{
cookies:{get(name:string){return cookieStore.get(name)?.value;},set(name:string, value:string, options: CookieOptions){
cookieStore.set({ name, value,...options });},remove(name:string, options: CookieOptions){
cookieStore.delete({ name,...options });},},});const{ error }=await supabase.auth.exchangeCodeForSession(code);if(!error){return NextResponse.redirect(`${origin}${next}`);}}// return the user to an error page with instructionsreturn NextResponse.redirect(`${origin}/auth/auth-code-error`);}
添加 Supabase 的 Auth Provider.
- 访问 https://supabase.com/dashboard/project/_/auth/providers,找到 Github,开启。
- 访问 https://github.com/settings/developers,点击 New Oauth App,Homepage URL 填入
http://localhost:3000/
, Authorization callback URL 填入 Supabase 提供的Callback URL (for OAuth)
,点击 Register Application。 - 点击 Generate a new client secret,复制秘钥。
- 将 Client ID 和 Client Secret 分别填入,点击 Save。
修改
app/page.tsx
:
"use client";
import { createBrowserClient } from "@supabase/ssr";
import { Github } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function Home() {
const pathname = usePathname;
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const handleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: location.origin + "/auth/callback?next=" + pathname,
},
});
};
const handleLogout = async () => {
await supabase.auth.signOut();
};
return (
<div className="flex gap-2">
<Button onClick={handleLogin} className="flex items-center gap-1">
<Github size={18} />
Login
</Button>
<Button onClick={handleLogout} className="flex items-center gap-1">
<LogOut size={18} />
Logout
</Button>
</div>
);
}
点击登录按钮,认证成功将返回首页
http://localhost:3000
,此时我们已经完成了最基础的登录登出功能。
用户信息缓存
安装 zustand
pnpminstall zustand
创建文件
/lib/store/user.ts
:
import{ create }from"zustand";import{ User }from"@supabase/supabase-js";interfaceUserState{
user: User |undefined;setUser:(user: User |undefined)=>void;}exportconst useUser =create<UserState>((set)=>({
user:undefined,setUser:(user)=>set(()=>({ user })),}));
创建文件
/components/session-provider.tsx
:
"use client";
import { useUser } from "@/lib/store/user";
import { createBrowserClient } from "@supabase/ssr";
import { useCallback, useEffect } from "react";
const SessionProvider = () => {
const setUser = useUser((state) => state.setUser);
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const userSession = useCallback(async () => {
const { data } = await supabase.auth.getSession();
setUser(data.session?.user);
}, [setUser, supabase]);
useEffect(() => {
userSession();
}, [userSession]);
return null
};
export default SessionProvider;
在
/app/layout.tsx
中引入 session-provider。
import SessionProvider from "@/components/session-provider";
...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<SessionProvider />
</body>
</html>
);
}
修改
/app/page.tsx
:
"use client";
import { createBrowserClient } from "@supabase/ssr";
import { Github, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUser } from "@/lib/store/user";
export default function Home() {
const pathname = usePathname;
const user = useUser((state) => state.user);
const setUser = useUser((state) => state.setUser);
console.log(user, "user");
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const handleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: "github",
options: {
redirectTo: location.origin + "/auth/callback?next=" + pathname,
},
});
};
const handleLogout = async () => {
await supabase.auth.signOut();
setUser(undefined);
};
return (
<div>
<h1 className="text-2xl py-2">Hi: {user?.user_metadata?.user_name}</h1>
<div className="flex gap-2">
{!user?.id ? (
<Button onClick={handleLogin} className="flex items-center gap-1">
<Github size={18} />
Login
</Button>
) : (
<Button onClick={handleLogout} className="flex items-center gap-1">
<LogOut size={18} />
Logout
</Button>
)}
</div>
</div>
);
}
同步数据表
访问 https://supabase.com/dashboard/project/_/database/tables,点击 New table,创建一张 users 数据表。
创建完成后进入 SQL Editor 填入下面两个 SQL,执行。
-- 创建 create_user_on_signup 函数,1.在 public.users 表中插入一条新的记录。2.更新 auth.users 表中的 raw_user_meta_data 字段。CREATEFUNCTION create_user_on_signup()RETURNSTRIGGERAS $$
BEGININSERTINTOpublic.users (id, email, user_name, image_url)VALUES(
NEW.id,
NEW.raw_user_meta_data ->>'email',
NEW.raw_user_meta_data ->>'user_name',
NEW.raw_user_meta_data ->>'avatar_url');UPDATE auth.users SET raw_user_meta_data = raw_user_meta_data ||'{"role": "user"}'::jsonb WHERE auth.users.id = NEW.id;RETURN NEW;END;
$$ language plpgsql security definer;-- 创建触发器,当 auth.users 表新增用户后自动触发 create_user_on_signup 函数CREATETRIGGER create_user_on_signup afterINSERTON auth.users FOR EACH ROWEXECUTEFUNCTION create_user_on_signup();
- 进入 Authentication 将已经授权的用户删除
- 重新点击登录。此时 users 表已经同步新增了一条数据。
路由守卫
我们的一些页面是不希望未登录用户或者普通用户进行访问的,于是需要对页面进行拦截。
修改
middleware.ts
文件:
exportasyncfunctionmiddleware(request: NextRequest){...const{ data }=await supabase.auth.getSession();if(!data.session || data.session.user.user_metadata.role !=='admin'){return NextResponse.redirect(newURL('/', request.url));}return response;}exportconst config ={
matcher:["/admin/:path*"],};
新建
/app/admin/page.ts
:
const AdminPage = () => {
return <div>admin page</div>;
};
export default AdminPage;
此时访问 http://localhost:3000/admin 会被重定向到首页。
进入 SQL Editor 修改我们的权限:
UPDATE users SET role ='admin'WHERE id ='254ec4d1-a5bb-46de-9a29-134aa59ddfcb';UPDATE auth.users SET raw_user_meta_data = raw_user_meta_data ||'{"role": "admin"}'::jsonb WHERE auth.users.id ='254ec4d1-a5bb-46de-9a29-134aa59ddfcb';
id 可以在 user 表或者 Authentication 页面中复制。
退出重新登录,再次访问 /admin 页面,成功进入。
版权归原作者 远小帅 所有, 如有侵权,请联系我们删除。