mcpField
C
clerk-authentication

clerk-authentication

github.com/mindrally安全287687 次安装3 周前更新
代码仓库

关于 clerk-authentication

面向 Next.js 开发者的 Clerk 身份验证实现指南,涵盖服务端与客户端校验、路由和操作保护及安全实践。

主要功能

  1. 01服务端与客户端身份验证模式
  2. 02保护服务端操作和 API 路由
  3. 03组织与角色校验
  4. 04JWT 和自定义会话声明
  5. 05Clerk UI 组件与测试指南

适用场景

  1. 01为 Next.js 页面添加登录校验
  2. 02保护服务端操作和 API 处理程序
  3. 03按组织或角色限制数据访问

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mindrally/skills --skill clerk-authentication

安装后在 Claude Code 中输入/clerk-authentication确认已加载。

详细介绍

Clerk 身份验证

你是 Next.js 应用中 Clerk 身份验证实现方面的专家。集成 Clerk 时请遵循以下指南。

核心原则

  • 通过多个身份验证层实现纵深防御
  • 在每个数据访问点验证身份,而不仅仅依赖中间件
  • 单独保护每个服务端操作
  • 使用 Clerk 内置的安全功能(HttpOnly Cookie、CSRF 保护)

安装与设置

npm install @clerk/nextjs
环境变量
# Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...

# Optional: Custom URLs
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

提供程序设置

App Router(app/layout.tsx)
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}
使用自定义外观
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider
      appearance={{
        baseTheme: dark,
        variables: {
          colorPrimary: '#3b82f6',
        },
        elements: {
          formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
        },
      }}
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

中间件配置

基本中间件(middleware.ts)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

// Define protected routes
const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/api/protected(.*)',
  '/settings(.*)',
]);

// Define public routes (optional, for clarity)
const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/api/public(.*)',
]);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: [
    // Skip Next.js internals and static files
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)',
  ],
};
带有基于角色的访问控制的高级中间件
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isAdminRoute = createRouteMatcher(['/admin(.*)']);
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/protected(.*)']);

export default clerkMiddleware(async (auth, req) => {
  const { userId, sessionClaims } = await auth();

  // Admin routes require admin role
  if (isAdminRoute(req)) {
    if (!userId || sessionClaims?.metadata?.role !== 'admin') {
      return new Response('Forbidden', { status: 403 });
    }
  }

  // Protected routes require authentication
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

在服务端组件中进行身份验证

使用 auth()
import { auth } from '@clerk/nextjs/server';

export default async function DashboardPage() {
  const { userId } = await auth();

  if (!userId) {
    redirect('/sign-in');
  }

  // Fetch user-specific data
  const data = await fetchUserData(userId);

  return <Dashboard data={data} />;
}
使用 currentUser()
import { currentUser } from '@clerk/nextjs/server';

export default async function ProfilePage() {
  const user = await currentUser();

  if (!user) {
    redirect('/sign-in');
  }

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Email: {user.emailAddresses[0]?.emailAddress}</p>
    </div>
  );
}

在客户端组件中进行身份验证

useUser Hook
'use client';

import { useUser } from '@clerk/nextjs';

export function UserProfile() {
  const { isLoaded, isSignedIn, user } = useUser();

  if (!isLoaded) {
    return <Skeleton />;
  }

  if (!isSignedIn) {
    return <SignInPrompt />;
  }

  return (
    <div>
      <img src={user.imageUrl} alt={user.fullName ?? 'User'} />
      <p>{user.fullName}</p>
    </div>
  );
}
useAuth Hook
'use client';

import { useAuth } from '@clerk/nextjs';

export function ProtectedAction() {
  const { isLoaded, userId, getToken } = useAuth();

  async function handleAction() {
    if (!userId) return;

    // Get a fresh token for API calls
    const token = await getToken();

    const response = await fetch('/api/protected', {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
  }

  if (!isLoaded || !userId) {
    return null;
  }

  return <button onClick={handleAction}>Perform Action</button>;
}

保护服务端操作

始终单独保护每个服务端操作:

'use server';

import { auth } from '@clerk/nextjs/server';

export async function createPost(formData: FormData) {
  const { userId } = await auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Create post with user ID
  const post = await db.post.create({
    data: {
      title,
      content,
      authorId: userId,
    },
  });

  revalidatePath('/posts');
  return post;
}
使用角色验证
'use server';

import { auth } from '@clerk/nextjs/server';

export async function deleteUser(userId: string) {
  const { userId: currentUserId, sessionClaims } = await auth();

  if (!currentUserId) {
    throw new Error('Unauthorized');
  }

  if (sessionClaims?.metadata?.role !== 'admin') {
    throw new Error('Forbidden: Admin access required');
  }

  await db.user.delete({ where: { id: userId } });
  revalidatePath('/admin/users');
}

保护 API 路由

路由处理程序(App Router)
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const data = await fetchUserData(userId);
  return NextResponse.json(data);
}

export async function POST(request: Request) {
  const { userId } = await auth();

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();
  // Process request...

  return NextResponse.json({ success: true });
}

外部 API 的 JWT 验证

import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { getToken } = await auth();

  // Get JWT for external API
  const token = await getToken({ template: 'external-api' });

  const response = await fetch('https://external-api.com/data', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return Response.json(await response.json());
}

组织支持

import { auth } from '@clerk/nextjs/server';

export async function getOrganizationData() {
  const { userId, orgId, orgRole } = await auth();

  if (!userId || !orgId) {
    throw new Error('Must be in an organization');
  }

  // Check organization role
  if (orgRole !== 'org:admin') {
    throw new Error('Admin access required');
  }

  return await db.organization.findUnique({
    where: { clerkOrgId: orgId },
  });
}

自定义会话声明

在 Clerk Dashboard 中配置

通过 JWT Templates 添加自定义声明,然后访问这些声明:

import { auth } from '@clerk/nextjs/server';

export async function checkSubscription() {
  const { sessionClaims } = await auth();

  const plan = sessionClaims?.metadata?.subscriptionPlan;

  if (plan !== 'pro') {
    throw new Error('Pro subscription required');
  }
}

UI 组件

预构建组件
import {
  SignIn,
  SignUp,
  SignOutButton,
  UserButton,
  SignedIn,
  SignedOut,
} from '@clerk/nextjs';

export function Header() {
  return (
    <header>
      <SignedIn>
        <UserButton afterSignOutUrl="/" />
      </SignedIn>
      <SignedOut>
        <SignInButton mode="modal" />
      </SignedOut>
    </header>
  );
}

// Dedicated sign-in page
export default function SignInPage() {
  return (
    <div className="flex justify-center items-center min-h-screen">
      <SignIn />
    </div>
  );
}

安全最佳实践

1. 纵深防御
// Layer 1: Middleware
export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

// Layer 2: Server Component
export default async function Page() {
  const { userId } = await auth();
  if (!userId) redirect('/sign-in');
  // ...
}

// Layer 3: Data Access
async function fetchUserData(userId: string) {
  const { userId: currentUserId } = await auth();
  if (currentUserId !== userId) throw new Error('Forbidden');
  // ...
}
2. 保护所有服务端操作
// Every server action should verify auth independently
'use server';

export async function sensitiveAction() {
  const { userId } = await auth();
  if (!userId) throw new Error('Unauthorized');
  // ...
}
3. 避免仅在客户端进行保护
// BAD: Client-side only check
'use client';
export function SecretComponent() {
  const { isSignedIn } = useAuth();
  if (!isSignedIn) return null;
  return <div>Secret Data</div>; // Data still sent to client!
}

// GOOD: Server-side protection
export default async function SecretPage() {
  const { userId } = await auth();
  if (!userId) redirect('/sign-in');
  const data = await fetchSecretData(userId);
  return <SecretComponent data={data} />;
}

错误处理

import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

export default async function ProtectedPage() {
  try {
    const { userId } = await auth();

    if (!userId) {
      redirect('/sign-in');
    }

    const data = await fetchUserData(userId);
    return <Dashboard data={data} />;
  } catch (error) {
    if (error instanceof AuthenticationError) {
      redirect('/sign-in');
    }
    throw error;
  }
}

测试

// Mock Clerk for testing
import { auth } from '@clerk/nextjs/server';

jest.mock('@clerk/nextjs/server', () => ({
  auth: jest.fn(),
}));

describe('Protected API', () => {
  it('returns 401 for unauthenticated requests', async () => {
    (auth as jest.Mock).mockResolvedValue({ userId: null });

    const response = await GET();
    expect(response.status).toBe(401);
  });

  it('returns data for authenticated requests', async () => {
    (auth as jest.Mock).mockResolvedValue({ userId: 'user_123' });

    const response = await GET();
    expect(response.status).toBe(200);
  });
});

应避免的常见反模式

  1. 仅依赖中间件进行保护
  2. 不单独保护服务端操作
  3. 使用客户端身份验证检查来保护敏感数据
  4. 未验证所有权便公开用户数据
  5. 未验证组织范围资源的组织成员资格
  6. 将角色检查硬编码,而不使用 Clerk 的 RBAC
  7. 未处理客户端组件中的加载状态

评论

支持 Markdown