Next.js使用Supabase实现Github登录

Next.js 有许多 OAuth 认证方案来实现 Github 或者 Google 登录,比较常见的有 next-authclerksupabase等。Supabase提供了很多的核心服务,包括 PostgreSQL 数据库、身份验证、文件存储等。

本文将介绍如何使用 Supabase 实现 Github 登录,您将学到:

  1. 使用 OAuth 认证登录。
  2. 使用 Github 注册自动创建用户表数据。
  3. 用户数据缓存(zustand)。
  4. 路由守卫。

在继续开始前,您需要具备以下的基本知识:

  • Node.js
  • npm/pnpm
  • Next.js

起步

项目初始化

使用 pnpm 创建最新的 Next.js 项目。

Node.js 版本至少需要 v18.17。

shell 复制代码
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 import alias (@/*)? ... 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

在终端启动项目:

shell 复制代码
pnpm run dev

浏览器打开 http://localhost:3000/ 将看到:

创建 Supabase 项目

  1. 首先进入 supabase 创建一个账户。
  2. 登录成功后进入 dashboard ,点击 New project。
  3. 设置项目名、数据库密码以及所属地区。

开始

为了实现一个好看的页面,我这里将使用 shadcn-ui 来作为项目的 ui 组件库。

安装 shadcn-ui:

shell 复制代码
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 import alias for components: ... @/components
√ Configure the import alias for utils: ... @/lib/utils
√ Are you using React Server Components? ... no / yes
√ Write configuration to components.json. Proceed? ... yes

添加 Button 按钮:

shell 复制代码
pnpm dlx shadcn-ui@latest add button

添加 Lucide 图标库:

shell 复制代码
pnpm install lucide-react

修改 app/page.tsx

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 包

shell 复制代码
pnpm install @supabase/ssr @supabase/supabase-js

在项目根目录新建一个 .env.local 文件,SUPABASE_URL 和 SUPABASE_ANON_KEY 可以在 supabase.com/dashboard/p... 中获取。

ini 复制代码
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key

在根目录新建 middleware.ts 文件。输入以下内容:

ts 复制代码
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(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 文件:

ts 复制代码
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { type CookieOptions, createServerClient } from "@supabase/ssr";

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get("code");
  // if "next" is in param, use it as the redirect URL
  const 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 instructions
  return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}

添加 Supabase 的 Auth Provider.

  1. 访问 supabase.com/dashboard/p...,找到 Github,开启。
  2. 访问 github.com/settings/de...,点击 New Oauth App,Homepage URL 填入 http://localhost:3000/ , Authorization callback URL 填入 Supabase 提供的 Callback URL (for OAuth),点击 Register Application。
  3. 点击 Generate a new client secret,复制秘钥。
  4. 将 Client ID 和 Client Secret 分别填入,点击 Save。

修改 app/page.tsx

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

shell 复制代码
pnpm install zustand

创建文件 /lib/store/user.ts

ts 复制代码
import { create } from "zustand";
import { User } from "@supabase/supabase-js";

interface UserState {
  user: User | undefined;
  setUser: (user: User | undefined) => void;
}

export const useUser = create<UserState>((set) => ({
  user: undefined,
  setUser: (user) => set(() => ({ user })),
}));

创建文件 /components/session-provider.tsx

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。

tsx 复制代码
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

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>
  );
}

同步数据表

访问 supabase.com/dashboard/p...,点击 New table,创建一张 users 数据表。

创建完成后进入 SQL Editor 填入下面两个 SQL,执行。

sql 复制代码
-- 创建 create_user_on_signup 函数,1.在 public.users 表中插入一条新的记录。2.更新 auth.users 表中的 raw_user_meta_data 字段。
CREATE FUNCTION create_user_on_signup() RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO public.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 函数
CREATE TRIGGER create_user_on_signup after INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION create_user_on_signup();
  1. 进入 Authentication 将已经授权的用户删除
  2. 重新点击登录。此时 users 表已经同步新增了一条数据。

路由守卫

我们的一些页面是不希望未登录用户或者普通用户进行访问的,于是需要对页面进行拦截。

修改 middleware.ts 文件:

ts 复制代码
export async function middleware(request: NextRequest) {
	...
	const { data } = await supabase.auth.getSession();

  if (!data.session || data.session.user.user_metadata.role !== 'admin') {
    return NextResponse.redirect(new URL('/', request.url));
  }

  return response;
}

export const config = {
  matcher: ["/admin/:path*"],
};

新建 /app/admin/page.ts

tsx 复制代码
const AdminPage = () => {
  return <div>admin page</div>;
};

export default AdminPage;

此时访问 http://localhost:3000/admin 会被重定向到首页。

进入 SQL Editor 修改我们的权限:

sql 复制代码
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 页面,成功进入。

相关推荐
Redstone Monstrosity12 分钟前
字节二面
前端·面试
东方翱翔19 分钟前
CSS的三种基本选择器
前端·css
Fan_web42 分钟前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel