用 Claude Code 实现社交登录: Next.js、Auth.js、Google 与 GitHub OAuth 安全指南
用 Claude Code 构建安全社交登录,涵盖 Next.js/Auth.js、Google/GitHub OAuth、代码示例和上线陷阱。
社交登录并不是把注册表单缩短那么简单。只要让用户通过 Google 或 GitHub OAuth 进入产品,你就同时在设计身份确认、账号绑定、Session、Cookie、CSRF、防止过度授权、日志审计和客服处理流程。直接让 Claude Code “做一个登录” 往往能得到一个按钮,但也可能留下 redirect URI 不一致、未验证邮箱自动绑定、scope 过大、client secret 写进代码等问题。
本文以 Next.js App Router、Auth.js(NextAuth v5 风格 API)、TypeScript 和 Prisma Adapter 为例,整理一套可以交给 Claude Code 执行、也方便人工审查的社交登录实现方式。OAuth authorization code 可以理解为“临时兑换券”,state 是“防止 CSRF 的随机值”,redirect URI 是“OAuth 提供方回跳的精确地址”,account linking 则是“用户明确同意后把另一个登录方式绑定到同一个账号”。
先确定安全边界
不要先问“要接几个 provider”,而要先问“这次登录要证明什么”。一般 SaaS、课程报名、开发者工具,Google 和 GitHub 已经足够。Google 适合普通用户和企业邮箱,GitHub 适合开发者产品、技术社区和研修报名。登录阶段不应请求 Google Drive、Calendar 或 GitHub repo 权限,除非用户已经进入真正需要这些权限的功能。
适合交给 Claude Code 的任务粒度如下。
- 添加 Auth.js provider、环境变量和 callback route
- 实现登录页和受保护页面
- Session 只扩展
user.id,不要把 access token 传给浏览器 - 账号绑定只能由已登录用户在设置页主动发起
- 禁止删除最后一个登录方式
- client secret、refresh token、access token 不得写入代码、日志、Issue 或文章草稿
Masa 在测试项目中踩过的坑,不是代码生成失败,而是 Google Cloud Console 中的 redirect URI 和应用侧 AUTH_URL 少了一个环境差异。Claude Code 擅长改代码,但无法自动知道外部控制台的设置。因此,URL、scope、邮箱验证规则、Cookie 策略必须先由人整理出来。
OAuth 流程图
Google 官方说明中也强调,Web 应用更推荐 authorization code flow。浏览器拿到短期 code 后,由服务器使用 client secret 去 token endpoint 交换。client secret 永远不应该进入浏览器代码。
sequenceDiagram
participant User as User
participant App as Next.js app
participant Auth as Auth.js route
participant Provider as Google or GitHub
User->>App: Click "Continue with Google"
App->>Auth: signIn("google")
Auth->>Provider: Redirect with client_id, redirect_uri, scope, state
Provider->>User: Consent screen
Provider->>Auth: Redirect back with code and state
Auth->>Provider: Exchange code on the server
Provider->>Auth: Return tokens
Auth->>App: Create session cookie
App->>User: Show dashboard
state 用来确认回调确实属于刚才发起的登录请求。使用 Auth.js 标准 route 时,它会处理 state、CSRF Cookie 和 callback 交换。如果你让 Claude Code 自己写 callback route,必须明确要求验证 state。GitHub 官方文档也建议使用不可预测的 state,并在不匹配时中止流程。
环境变量和最小 scope
下面的配置以登录为目标,而不是在首次登录时顺便拿走大量 API 权限。Google 只使用openid email profile,GitHub 只使用read:user user:email。
npm install next-auth@beta @auth/prisma-adapter prisma @prisma/client
npx prisma init
npm exec auth secret
# .env.local
AUTH_SECRET="由 npm exec auth secret 生成的值"
AUTH_URL="http://localhost:3000"
AUTH_GOOGLE_ID="Google Cloud Console 的 client ID"
AUTH_GOOGLE_SECRET="Google Cloud Console 的 client secret"
AUTH_GITHUB_ID="GitHub OAuth App 的 client ID"
AUTH_GITHUB_SECRET="GitHub OAuth App 的 client secret"
DATABASE_URL="postgresql://user:password@localhost:5432/app"
.env.example 只放空键,不能放真实值。
# .env.example
AUTH_SECRET=
AUTH_URL=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
DATABASE_URL=
Google 的 Authorized redirect URI 在本地应为http://localhost:3000/api/auth/callback/google,生产环境应为https://example.com/api/auth/callback/google。GitHub OAuth App 则使用https://example.com/api/auth/callback/github。域名、协议、路径和 provider 名称都要完全一致。
可复制的 Auth.js 配置
下面的auth.ts会拒绝未验证的 Google 邮箱,并拒绝没有返回邮箱的 GitHub 登录。它也不会把 provider token 放进客户端 Session。
// auth.ts
import NextAuth, { type NextAuthConfig } from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
type GoogleProfile = {
sub: string;
name?: string;
email: string;
email_verified: boolean;
picture?: string;
};
export const authConfig = {
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
providers: [
Google({
authorization: {
params: {
scope: "openid email profile",
response_type: "code",
},
},
profile(profile: GoogleProfile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
emailVerified: profile.email_verified ? new Date() : null,
};
},
}),
GitHub({
authorization: {
params: {
scope: "read:user user:email",
},
},
}),
],
callbacks: {
async signIn({ account, profile, user }) {
if (account?.provider === "google") {
const googleProfile = profile as GoogleProfile | undefined;
return Boolean(googleProfile?.email && googleProfile.email_verified);
}
if (account?.provider === "github") {
return Boolean(user.email);
}
return true;
},
async session({ session, user }) {
session.user.id = user.id;
return session;
},
},
pages: {
signIn: "/login",
error: "/login",
},
} satisfies NextAuthConfig;
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig);
// src/types/next-auth.d.ts
import "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
name?: string | null;
email?: string | null;
image?: string | null;
};
}
}
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
如果你使用 Prisma Adapter,请让 Claude Code 同时添加 Auth.js 标准的User、Account、Session、VerificationToken模型。Account表中的provider和providerAccountId是后续账号绑定审计的基础。
登录页和受保护页面
通过 Server Action 调用signIn,可以让请求进入 Auth.js 标准流程,而不是手写 provider URL 字符串。
// src/app/login/page.tsx
import { signIn } from "@/auth";
const providers = [
{ id: "google", label: "使用 Google 继续" },
{ id: "github", label: "使用 GitHub 继续" },
] as const;
export default function LoginPage({
searchParams,
}: {
searchParams: { error?: string };
}) {
return (
<main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 px-6">
<div>
<h1 className="text-2xl font-bold">登录</h1>
<p className="mt-2 text-sm text-gray-600">
请选择用于访问此应用的工作账号。
</p>
</div>
{searchParams.error ? (
<p className="rounded-md bg-red-50 p-3 text-sm text-red-700">
登录失败。请尝试其他账号或联系管理员。
</p>
) : null}
<div className="grid gap-3">
{providers.map((provider) => (
<form
key={provider.id}
action={async () => {
"use server";
await signIn(provider.id, { redirectTo: "/dashboard" });
}}
>
<button
type="submit"
className="w-full rounded-md border px-4 py-3 text-sm font-medium hover:bg-gray-50"
>
{provider.label}
</button>
</form>
))}
</div>
</main>
);
}
// src/app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<main className="mx-auto max-w-3xl p-8">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="mt-4 text-gray-700">
Signed in as {session.user.email}.
</p>
</main>
);
}
账号绑定和解除
不要因为两个 provider 返回同一个邮箱就自动合并账号。更安全的做法是,用户已经登录后,在设置页主动点击“绑定 Google”或“绑定 GitHub”。解除时必须阻止删除最后一个登录方式,并对自定义 API 做同源检查。
// src/app/api/settings/linked-accounts/route.ts
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
function isSameOrigin(request: Request) {
const origin = request.headers.get("origin");
const host = request.headers.get("host");
if (!origin || !host) return false;
try {
return new URL(origin).host === host;
} catch {
return false;
}
}
export async function DELETE(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isSameOrigin(request)) {
return NextResponse.json({ error: "Bad origin" }, { status: 403 });
}
const body = (await request.json()) as { provider?: string };
const accounts = await prisma.account.findMany({
where: { userId: session.user.id },
select: { provider: true },
});
if (accounts.length <= 1) {
return NextResponse.json(
{ error: "不能删除最后一个登录方式。" },
{ status: 400 },
);
}
await prisma.account.deleteMany({
where: { userId: session.user.id, provider: body.provider },
});
return NextResponse.json({ ok: true });
}
Claude Code 提示词和审查表
请为 Next.js App Router 应用添加 Auth.js v5 的 Google/GitHub 社交登录。
要求:
- client secret 只能从 .env.local 或部署平台 secret 读取
- Google scope 只能是 openid email profile
- GitHub scope 只能是 read:user user:email
- Google 登录必须检查 email_verified
- Session 只追加 user.id,不向客户端暴露 access_token
- 账号绑定只能由已登录用户在设置页发起
- 不允许解除最后一个登录方式
- 完成后输出 lint 结果和手动 OAuth 检查步骤
| 审查点 | 要确认的内容 |
|---|---|
| OAuth flow | 使用 authorization code flow,redirect URI 与控制台完全一致 |
| state 和 CSRF | Auth.js 标准 route 未被绕开,自定义 API 有同源检查 |
| Cookie 和 Session | 生产环境 Cookie 使用 Secure、HttpOnly、SameSite 思路,token 留在服务器 |
| Account linking | 只在 verified email 和用户明确操作后绑定账号 |
真实使用场景
第一个场景是 B2B SaaS 管理后台。Google 作为主登录方式,后续可以增加 Workspace 域名限制和角色权限。GitHub 只给开发者或技术支持人员使用。
第二个场景是开发者工具。首次登录只用 GitHub 身份,真正进入仓库功能时再请求额外权限。这样同意页面更轻,转化率也更高。
第三个场景是已有邮箱密码系统增加社交登录。正确做法是让已登录用户在设置页绑定 Google 或 GitHub,而不是在未登录状态下看到同邮箱就自动合并。
第四个场景是研修、讲座或咨询报名页。Google verified email 能减少虚假报名和后续人工确认成本,同时让表单更短。
上线后常见失败
redirect URI 不一致最常见。本地可用、生产失败时,先检查AUTH_URL、反向代理 Host header、Google/GitHub 控制台 callback URL 是否完全一致。
邮箱自动绑定是更隐蔽的风险。Google 返回email_verified,但并不是所有 provider 都提供同等级别保证。不要把“字符串相同的邮箱”当作同一人的充分证据。
scope 过大也会损害 PV 和咨询转化。用户只是想登录,却看到仓库或文件权限请求,会直接离开。登录和后续 API 授权应该拆开。
client secret 直写必须在 review 中拦下。即使 Claude Code 生成的是示例字符串,也不能把真实值粘进去。使用.env.local、部署平台 secret 或专用 secret manager。
Google refresh token 也要单独设计。Google 可能只在首次同意时返回 refresh token。纯登录不应保存 refresh token;后台调用 Google API 时再做 token rotation 任务。
官方资料和内部链接
实现前请查一次一手资料: Auth.js、Google Identity Services OAuth 说明、GitHub OAuth Apps 授权文档、OWASP Authentication Cheat Sheet。
相关内部文章包括Claude Code OAuth 实现指南、Claude Code JWT 认证、Claude Code 安全最佳实践和环境变量管理。
咨询、研修和实际检查点
ClaudeCodeLab 可以帮助团队把“登录按钮”提升为可审查的认证流程,包括需求整理、Claude Code 任务拆分、代码 review、secret 管理和手动 OAuth 测试。若目标是提高咨询转化,建议先整理目标用户、provider、最小权限和错误处理,再进入实现。
实际尝试本文内容时,请确认开发和生产 redirect URI 完全一致、Google email_verified 已检查、GitHub 私有邮箱失败时有清晰提示、state 和 Cookie 仍走 Auth.js 标准流程、最后一个登录方式不能删除、client secret 没有出现在代码和日志中。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。