因为最近在做 Olinkics,用到了 Supabase,所以这段时间都在研究它。今天这篇文章主要分享下 Supabase Auth 这一块。
一、为什么选择 OAuth
一开始我计划的是让用户通过邮箱来注册账号,后面发现这种方式比较麻烦。
一是,你不仅仅是提供注册和登录功能就好了,你还需要提供重置密码的功能,这样用户在他们忘记密码时才能重置密码。这意味着我需要再画一些 UI,虽然也不难,但毫无疑问增加了我的工作量,而我很懒。
二是,重置密码一般来说需要由你发送一封重置密码的邮件到用户的邮箱,这就涉及到邮件发送服务,虽然 Supabase 有提供这个服务,但是发送邮件是有次数限制的,并且他也推荐你自己搞一个 STMP server,这对我来说就显得有点麻烦了。
所以最后我决定放弃邮箱/密码这种组合方式,改成用 OAuth,将 Auth 这块的功能“外包”给第三方。而这个方式我看到最常用的就是 Google 和 Apple,后者由于我没有苹果账号肯定是搞不了,所以最后选择了前者,当然这意味着到时用户必须翻墙才能使用。
二、谷歌登录
具体怎么做呢?Supabase 的文档已经给出了非常详细的说明。
首先跟着 文档里的这部分 先后在 Google 和 Supabase 这两个地方完成配置。
然后回到代码端,跟着 文档里的这部分 完成一个 PKCE flow 的 Server-Side Auth:
src\app\(auth)\login\page.tsx
import { signin } from "./actions";
export default function LoginPage() {
return (
<form>
<button formAction={signin}>Log in</button>
</form>
);
}
src\app\(auth)\login\actions.ts
"use server";
import { createClient } from "@/utils/supabase/server";
export async function signin() {
const supabase = await createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: "http://localhost:3000/callback", // 🔴 根据自己的实际项目修改
},
});
if (data.url) {
redirect(data.url); // use the redirect API for your server framework
}
}
↓
"use server";
import { getURL } from "@/utils/helpers";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export async function signin() {
const supabase = createClient();
const redirectURL = getURL("/callback");
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: redirectURL,
},
});
if (error) {
redirect(`/login?message=${error.message}`);
}
redirect(data.url);
}
src\app\(auth)\callback\route.ts
import { NextResponse } from "next/server";
// The client you created from the Server-Side Auth instructions
import { createClient } from "@/utils/supabase/server";
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") ?? "/mine"; // 🔴 根据自己的实际项目修改
if (code) {
const supabase = createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === "development";
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`);
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`);
} else {
return NextResponse.redirect(`${origin}${next}`);
}
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}
三、登出
最后是登出,这个跟着文档走也可以搞定。
src\app\(private)\mine\page.tsx
import { signOut } from "./actions";
export default async function page() {
return (
<form>
<button formAction={signOut}>sign out</button>
</form>
);
}
src\app\(private)\mine\actions.ts
"use server";
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
export async function signOut() {
const supabase = createClient();
await supabase.auth.signOut({ scope: "local" });
redirect("/login");
}
四、路由权限
For public routes:
src\app\page.tsx
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
export default async function page() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (user) {
redirect("/mine");
}
return <>...</>;
}
For private routes:
src\app\(private)\mine\page.tsx
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
export default async function page() {
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
return (
<div>{user.email}</div>
);
}
Bonus、helper function for constructing redirect url
参考 这里 的用于 generate redirect url 的 get site base url helper function:
export const getURL = (path: string = '') => {
// Check if NEXT_PUBLIC_SITE_URL is set and non-empty. Set this to your site URL in production env.
let url =
process?.env?.NEXT_PUBLIC_SITE_URL &&
process.env.NEXT_PUBLIC_SITE_URL.trim() !== ''
? process.env.NEXT_PUBLIC_SITE_URL
: // If not set, check for NEXT_PUBLIC_VERCEL_URL, which is automatically set by Vercel.
process?.env?.NEXT_PUBLIC_VERCEL_URL &&
process.env.NEXT_PUBLIC_VERCEL_URL.trim() !== ''
? process.env.NEXT_PUBLIC_VERCEL_URL
: // If neither is set, default to localhost for local development.
'http://localhost:3000/';
// Trim the URL and remove trailing slash if exists.
url = url.replace(/\/+$/, '');
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Ensure path starts without a slash to avoid double slashes in the final URL.
path = path.replace(/^\/+/, '');
// Concatenate the URL and the path.
return path ? `${url}/${path}` : url;
};