authentication-authorization-clerk

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Authentication & Authorization with Clerk

使用Clerk实现身份验证与授权

Why We Use Clerk

我们为什么选择Clerk

The Authentication Problem

身份认证的痛点

Building secure authentication from scratch requires:
  • Password hashing (bcrypt/Argon2 with proper salts)
  • Session management (secure cookies, expiration, renewal)
  • Password reset flows (secure token generation, email verification)
  • Account lockout (prevent brute force)
  • MFA support (TOTP, SMS, authenticator apps)
  • Social login (OAuth flows for Google, GitHub, etc.)
  • User database sync
  • Security best practices for all of the above
Time to implement securely: 2-4 weeks for experienced developers
For vibe coders using AI: High risk of security gaps
从零构建安全的身份认证体系需要实现以下功能:
  • 密码哈希处理(使用带盐值的bcrypt/Argon2算法)
  • 会话管理(安全Cookie、过期机制、续期逻辑)
  • 密码重置流程(安全令牌生成、邮箱验证)
  • 账户锁定机制(防止暴力破解)
  • MFA多因素认证支持(TOTP、短信、认证器应用)
  • 第三方社交登录(Google、GitHub等平台的OAuth流程)
  • 用户数据库同步
  • 上述所有环节的安全最佳实践落地
安全实现所需工期: 经验丰富的开发者也需要2-4周
对于借助AI的快速开发人员: 存在极高的安全漏洞风险

Real-World Custom Auth Failures

现实中的自定义认证故障案例

Ashley Madison Breach (2015): Custom authentication with weak password hashing. 32 million accounts compromised.
Dropbox Breach (2012): Custom authentication led to password hash database theft. 68 million accounts affected.
According to Veracode's 2024 report, applications using managed authentication services (like Clerk, Auth0) had 73% fewer authentication-related vulnerabilities than those with custom authentication.
Ashley Madison数据泄露事件(2015年): 采用弱密码哈希的自定义认证体系,3200万账户信息被泄露。
Dropbox数据泄露事件(2012年): 自定义认证缺陷导致密码哈希数据库被盗,6800万账户受到影响。
根据Veracode 2024年报告,使用托管认证服务(如Clerk、Auth0)的应用,其身份认证相关漏洞比使用自定义认证的应用少73%

Our Clerk Architecture

我们的Clerk架构

What Clerk Handles (So We Don't Have To)

Clerk负责的能力(免去我们自行开发的工作量)

  • ✅ Password hashing (bcrypt/Argon2)
  • ✅ Session management (secure cookies)
  • ✅ MFA (built-in support)
  • ✅ OAuth providers (Google, GitHub, etc.)
  • ✅ Email verification
  • ✅ Password reset flows
  • ✅ Account lockout
  • ✅ Security monitoring
  • ✅ Compliance (SOC 2, GDPR)
Clerk is SOC 2 certified: This means an independent auditor verified their security controls meet industry standards. We inherit that certification.
  • ✅ 密码哈希处理(bcrypt/Argon2)
  • ✅ 会话管理(安全Cookie)
  • ✅ MFA多因素认证(内置支持)
  • ✅ OAuth第三方登录(Google、GitHub等)
  • ✅ 邮箱验证
  • ✅ 密码重置流程
  • ✅ 账户锁定机制
  • ✅ 安全监控
  • ✅ 合规认证(SOC 2、GDPR)
Clerk已通过SOC 2认证: 意味着独立审计机构已验证其安全控制符合行业标准,我们的应用可直接继承该认证资质。

Implementation Files

实现文件说明

  • middleware.ts
    - Clerk authentication for protected routes
  • app/dashboard/*
    - Protected by middleware
  • Clerk manages its own session cookies
  • middleware.ts
    - 用于受保护路由的Clerk认证逻辑
  • app/dashboard/*
    - 由中间件保护的路由范围
  • Clerk自行管理其会话Cookie

Basic Authentication

基础身份认证

Server-Side Authentication (API Routes)

服务端身份认证(API路由)

typescript
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError } from '@/lib/errorHandler';

async function handler(request: NextRequest) {
  // Get current user
  const { userId } = await auth();

  if (!userId) {
    return handleUnauthorizedError('Authentication required');
  }

  // User is authenticated, proceed
  // Use userId to associate data with user
}
typescript
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError } from '@/lib/errorHandler';

async function handler(request: NextRequest) {
  // Get current user
  const { userId } = await auth();

  if (!userId) {
    return handleUnauthorizedError('Authentication required');
  }

  // User is authenticated, proceed
  // Use userId to associate data with user
}

Client-Side Authentication (Components)

客户端身份认证(组件)

typescript
'use client';

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

export function ProfileComponent() {
  const { isLoaded, userId, sessionId } = useAuth();
  const { isLoaded: userLoaded, user } = useUser();

  if (!isLoaded || !userLoaded) {
    return <div>Loading...</div>;
  }

  if (!userId) {
    return <div>Please sign in</div>;
  }

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Email: {user.primaryEmailAddress?.emailAddress}</p>
    </div>
  );
}
typescript
'use client';

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

export function ProfileComponent() {
  const { isLoaded, userId, sessionId } = useAuth();
  const { isLoaded: userLoaded, user } = useUser();

  if (!isLoaded || !userLoaded) {
    return <div>Loading...</div>;
  }

  if (!userId) {
    return <div>Please sign in</div>;
  }

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

Protecting Routes with Middleware

使用中间件保护路由

typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

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

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

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};
typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

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

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

export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};

Authorization Patterns

授权模式

Resource Ownership Verification

资源归属校验

typescript
// app/api/posts/[id]/route.ts
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const { userId } = await auth();
  if (!userId) {
    return handleUnauthorizedError();
  }

  // Get resource
  const post = await db.posts.findOne({ id: params.id });

  // Check ownership
  if (post.userId !== userId) {
    return handleForbiddenError('Only the post author can delete this post');
  }

  // User is authorized
  await db.posts.delete({ id: params.id });
  return NextResponse.json({ success: true });
}
typescript
// app/api/posts/[id]/route.ts
import { auth } from '@clerk/nextjs/server';
import { handleUnauthorizedError, handleForbiddenError } from '@/lib/errorHandler';

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const { userId } = await auth();
  if (!userId) {
    return handleUnauthorizedError();
  }

  // Get resource
  const post = await db.posts.findOne({ id: params.id });

  // Check ownership
  if (post.userId !== userId) {
    return handleForbiddenError('Only the post author can delete this post');
  }

  // User is authorized
  await db.posts.delete({ id: params.id });
  return NextResponse.json({ success: true });
}

Role-Based Access Control (RBAC)

基于角色的访问控制(RBAC)

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

export async function handler(request: NextRequest) {
  const { userId, sessionClaims } = await auth();

  if (!userId) {
    return handleUnauthorizedError();
  }

  // Check role
  const role = sessionClaims?.metadata?.role as string;

  if (role !== 'admin') {
    return handleForbiddenError('Admin access required');
  }

  // User has admin role
  // Proceed with admin operation
}
typescript
import { auth } from '@clerk/nextjs/server';

export async function handler(request: NextRequest) {
  const { userId, sessionClaims } = await auth();

  if (!userId) {
    return handleUnauthorizedError();
  }

  // Check role
  const role = sessionClaims?.metadata?.role as string;

  if (role !== 'admin') {
    return handleForbiddenError('Admin access required');
  }

  // User has admin role
  // Proceed with admin operation
}

Subscription-Based Authorization

基于订阅的授权

Server-Side (API Routes)

服务端(API路由)

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

export async function handler(request: NextRequest) {
  const { userId, sessionClaims } = await auth();

  if (!userId) {
    return handleUnauthorizedError();
  }

  // Check subscription plan
  const plan = sessionClaims?.metadata?.plan as string;

  if (plan === 'free_user') {
    return NextResponse.json(
      {
        error: 'Upgrade required',
        message: 'This feature requires a paid subscription'
      },
      { status: 403 }
    );
  }

  // User has paid subscription
  // Proceed with premium feature
}
typescript
import { auth } from '@clerk/nextjs/server';

export async function handler(request: NextRequest) {
  const { userId, sessionClaims } = await auth();

  if (!userId) {
    return handleUnauthorizedError();
  }

  // Check subscription plan
  const plan = sessionClaims?.metadata?.plan as string;

  if (plan === 'free_user') {
    return NextResponse.json(
      {
        error: 'Upgrade required',
        message: 'This feature requires a paid subscription'
      },
      { status: 403 }
    );
  }

  // User has paid subscription
  // Proceed with premium feature
}

Client-Side (Components)

客户端(组件)

typescript
'use client';

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

export function PremiumFeature() {
  return (
    <Protect
      condition={(has) => !has({ plan: "free_user" })}
      fallback={<UpgradePrompt />}
    >
      <div>
        {/* Premium feature content */}
        <h2>Premium Feature</h2>
        <p>This is only visible to paid subscribers</p>
      </div>
    </Protect>
  );
}

function UpgradePrompt() {
  return (
    <div className="upgrade-prompt">
      <h3>Upgrade Required</h3>
      <p>This feature is available on our paid plans</p>
      <a href="/pricing">View Plans</a>
    </div>
  );
}
typescript
'use client';

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

export function PremiumFeature() {
  return (
    <Protect
      condition={(has) => !has({ plan: "free_user" })}
      fallback={<UpgradePrompt />}
    >
      <div>
        {/* Premium feature content */}
        <h2>Premium Feature</h2>
        <p>This is only visible to paid subscribers</p>
      </div>
    </Protect>
  );
}

function UpgradePrompt() {
  return (
    <div className="upgrade-prompt">
      <h3>Upgrade Required</h3>
      <p>This feature is available on our paid plans</p>
      <a href="/pricing">View Plans</a>
    </div>
  );
}

Complete Protected API Route Example

完整的受保护API路由示例

typescript
// app/api/premium/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { validateRequest } from '@/lib/validateRequest';
import { safeTextSchema } from '@/lib/validation';
import {
  handleApiError,
  handleUnauthorizedError,
  handleForbiddenError
} from '@/lib/errorHandler';

async function generateHandler(request: NextRequest) {
  try {
    // 1. Authentication
    const { userId, sessionClaims } = await auth();
    if (!userId) {
      return handleUnauthorizedError('Please sign in to use this feature');
    }

    // 2. Authorization (subscription check)
    const plan = sessionClaims?.metadata?.plan as string;
    if (plan === 'free_user') {
      return handleForbiddenError('Premium subscription required');
    }

    // 3. Input validation
    const body = await request.json();
    const validation = validateRequest(safeTextSchema, body);
    if (!validation.success) {
      return validation.response;
    }

    const prompt = validation.data;

    // 4. Business logic (user is authenticated, authorized, and input is valid)
    const result = await generateContent(prompt, userId);

    return NextResponse.json({ result });

  } catch (error) {
    return handleApiError(error, 'premium-generate');
  }
}

export const POST = withRateLimit(withCsrf(generateHandler));

export const config = {
  runtime: 'nodejs',
};
typescript
// app/api/premium/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { validateRequest } from '@/lib/validateRequest';
import { safeTextSchema } from '@/lib/validation';
import {
  handleApiError,
  handleUnauthorizedError,
  handleForbiddenError
} from '@/lib/errorHandler';

async function generateHandler(request: NextRequest) {
  try {
    // 1. Authentication
    const { userId, sessionClaims } = await auth();
    if (!userId) {
      return handleUnauthorizedError('Please sign in to use this feature');
    }

    // 2. Authorization (subscription check)
    const plan = sessionClaims?.metadata?.plan as string;
    if (plan === 'free_user') {
      return handleForbiddenError('Premium subscription required');
    }

    // 3. Input validation
    const body = await request.json();
    const validation = validateRequest(safeTextSchema, body);
    if (!validation.success) {
      return validation.response;
    }

    const prompt = validation.data;

    // 4. Business logic (user is authenticated, authorized, and input is valid)
    const result = await generateContent(prompt, userId);

    return NextResponse.json({ result });

  } catch (error) {
    return handleApiError(error, 'premium-generate');
  }
}

export const POST = withRateLimit(withCsrf(generateHandler));

export const config = {
  runtime: 'nodejs',
};

User Metadata & Custom Claims

用户元数据与自定义声明

Storing User Metadata

存储用户元数据

Clerk allows you to store custom metadata with each user:
typescript
import { clerkClient } from '@clerk/nextjs/server';

// Update user metadata
async function updateUserPlan(userId: string, plan: string) {
  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      plan: plan  // Accessible by client
    },
    privateMetadata: {
      stripeCustomerId: 'cus_123'  // Server-only
    }
  });
}
Clerk允许你为每个用户存储自定义元数据:
typescript
import { clerkClient } from '@clerk/nextjs/server';

// Update user metadata
async function updateUserPlan(userId: string, plan: string) {
  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      plan: plan  // Accessible by client
    },
    privateMetadata: {
      stripeCustomerId: 'cus_123'  // Server-only
    }
  });
}

Accessing Metadata

访问元数据

Server-side:
typescript
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.plan;
Client-side:
typescript
const { user } = useUser();
const plan = user?.publicMetadata?.plan;
服务端:
typescript
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.plan;
客户端:
typescript
const { user } = useUser();
const plan = user?.publicMetadata?.plan;

Webhook Integration (User Sync)

Webhook集成(用户数据同步)

When users sign up or update their profile, sync to your database:
typescript
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';

export async function POST(request: NextRequest) {
  // Verify webhook signature
  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
    throw new Error('Missing CLERK_WEBHOOK_SECRET');
  }

  const headerPayload = headers();
  const svix_id = headerPayload.get("svix-id");
  const svix_timestamp = headerPayload.get("svix-timestamp");
  const svix_signature = headerPayload.get("svix-signature");

  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Missing svix headers', { status: 400 });
  }

  const payload = await request.json();
  const body = JSON.stringify(payload);

  const wh = new Webhook(WEBHOOK_SECRET);

  let evt: any;

  try {
    evt = wh.verify(body, {
      "svix-id": svix_id,
      "svix-timestamp": svix_timestamp,
      "svix-signature": svix_signature,
    });
  } catch (err) {
    console.error('Webhook verification failed:', err);
    return new Response('Invalid signature', { status: 400 });
  }

  // Handle different event types
  const { id, type, data } = evt;

  switch (type) {
    case 'user.created':
      await db.users.create({
        clerkId: data.id,
        email: data.email_addresses[0]?.email_address,
        firstName: data.first_name,
        lastName: data.last_name,
        createdAt: Date.now()
      });
      break;

    case 'user.updated':
      await db.users.update(
        { clerkId: data.id },
        {
          email: data.email_addresses[0]?.email_address,
          firstName: data.first_name,
          lastName: data.last_name,
          updatedAt: Date.now()
        }
      );
      break;

    case 'user.deleted':
      await db.users.delete({ clerkId: data.id });
      break;
  }

  return new Response('', { status: 200 });
}
当用户注册或更新个人资料时,可同步到你的数据库:
typescript
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';

export async function POST(request: NextRequest) {
  // Verify webhook signature
  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
    throw new Error('Missing CLERK_WEBHOOK_SECRET');
  }

  const headerPayload = headers();
  const svix_id = headerPayload.get("svix-id");
  const svix_timestamp = headerPayload.get("svix-timestamp");
  const svix_signature = headerPayload.get("svix-signature");

  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Missing svix headers', { status: 400 });
  }

  const payload = await request.json();
  const body = JSON.stringify(payload);

  const wh = new Webhook(WEBHOOK_SECRET);

  let evt: any;

  try {
    evt = wh.verify(body, {
      "svix-id": svix_id,
      "svix-timestamp": svix_timestamp,
      "svix-signature": svix_signature,
    });
  } catch (err) {
    console.error('Webhook verification failed:', err);
    return new Response('Invalid signature', { status: 400 });
  }

  // Handle different event types
  const { id, type, data } = evt;

  switch (type) {
    case 'user.created':
      await db.users.create({
        clerkId: data.id,
        email: data.email_addresses[0]?.email_address,
        firstName: data.first_name,
        lastName: data.last_name,
        createdAt: Date.now()
      });
      break;

    case 'user.updated':
      await db.users.update(
        { clerkId: data.id },
        {
          email: data.email_addresses[0]?.email_address,
          firstName: data.first_name,
          lastName: data.last_name,
          updatedAt: Date.now()
        }
      );
      break;

    case 'user.deleted':
      await db.users.delete({ clerkId: data.id });
      break;
  }

  return new Response('', { status: 200 });
}

Convex Integration

Convex集成

Using Clerk Auth with Convex

结合Convex使用Clerk认证

typescript
// convex/posts.ts
import { mutation, query } from "./_generated/server";

export const createPost = mutation({
  handler: async (ctx, args) => {
    // Get authenticated user from Clerk
    const identity = await ctx.auth.getUserIdentity();

    if (!identity) {
      throw new Error("Unauthenticated");
    }

    // Use Clerk user ID
    const userId = identity.subject;

    await ctx.db.insert("posts", {
      title: args.title,
      content: args.content,
      userId,  // Associate with Clerk user
      createdAt: Date.now()
    });
  }
});

export const getMyPosts = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();

    if (!identity) {
      return [];
    }

    // Return only current user's posts
    return await ctx.db
      .query("posts")
      .filter((q) => q.eq(q.field("userId"), identity.subject))
      .collect();
  }
});
typescript
// convex/posts.ts
import { mutation, query } from "./_generated/server";

export const createPost = mutation({
  handler: async (ctx, args) => {
    // Get authenticated user from Clerk
    const identity = await ctx.auth.getUserIdentity();

    if (!identity) {
      throw new Error("Unauthenticated");
    }

    // Use Clerk user ID
    const userId = identity.subject;

    await ctx.db.insert("posts", {
      title: args.title,
      content: args.content,
      userId,  // Associate with Clerk user
      createdAt: Date.now()
    });
  }
});

export const getMyPosts = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();

    if (!identity) {
      return [];
    }

    // Return only current user's posts
    return await ctx.db
      .query("posts")
      .filter((q) => q.eq(q.field("userId"), identity.subject))
      .collect();
  }
});

Sign-In/Sign-Up Components

登录/注册组件

Basic Sign-In Page

基础登录页面

typescript
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        appearance={{
          elements: {
            formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          }
        }}
        routing="path"
        path="/sign-in"
        afterSignInUrl="/dashboard"
        signUpUrl="/sign-up"
      />
    </div>
  );
}
typescript
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignIn
        appearance={{
          elements: {
            formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          }
        }}
        routing="path"
        path="/sign-in"
        afterSignInUrl="/dashboard"
        signUpUrl="/sign-up"
      />
    </div>
  );
}

Basic Sign-Up Page

基础注册页面

typescript
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';

export default function SignUpPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignUp
        appearance={{
          elements: {
            formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          }
        }}
        routing="path"
        path="/sign-up"
        afterSignUpUrl="/onboarding"
        signInUrl="/sign-in"
      />
    </div>
  );
}
typescript
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';

export default function SignUpPage() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <SignUp
        appearance={{
          elements: {
            formButtonPrimary: 'bg-blue-600 hover:bg-blue-700',
          }
        }}
        routing="path"
        path="/sign-up"
        afterSignUpUrl="/onboarding"
        signInUrl="/sign-in"
      />
    </div>
  );
}

User Button (Profile/Sign Out)

用户按钮(个人资料/登出)

typescript
// components/Header.tsx
'use client';

import { UserButton, useAuth } from '@clerk/nextjs';
import Link from 'next/link';

export function Header() {
  const { isSignedIn } = useAuth();

  return (
    <header>
      <nav>
        <Link href="/">Home</Link>
        {isSignedIn ? (
          <>
            <Link href="/dashboard">Dashboard</Link>
            <UserButton afterSignOutUrl="/" />
          </>
        ) : (
          <>
            <Link href="/sign-in">Sign In</Link>
            <Link href="/sign-up">Sign Up</Link>
          </>
        )}
      </nav>
    </header>
  );
}
typescript
// components/Header.tsx
'use client';

import { UserButton, useAuth } from '@clerk/nextjs';
import Link from 'next/link';

export function Header() {
  const { isSignedIn } = useAuth();

  return (
    <header>
      <nav>
        <Link href="/">Home</Link>
        {isSignedIn ? (
          <>
            <Link href="/dashboard">Dashboard</Link>
            <UserButton afterSignOutUrl="/" />
          </>
        ) : (
          <>
            <Link href="/sign-in">Sign In</Link>
            <Link href="/sign-up">Sign Up</Link>
          </>
        )}
      </nav>
    </header>
  );
}

Environment Configuration

环境配置

Required Environment Variables

必需的环境变量

bash
undefined
bash
undefined

.env.local

.env.local

Clerk (from Clerk Dashboard)

Clerk (from Clerk Dashboard)

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...

Clerk URLs (auto-configured by Clerk, but can override)

Clerk URLs (auto-configured by Clerk, but can override)

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=/onboarding
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=/onboarding

Clerk Frontend API (for CSP)

Clerk Frontend API (for CSP)

NEXT_PUBLIC_CLERK_FRONTEND_API_URL=https://your-app.clerk.accounts.dev
NEXT_PUBLIC_CLERK_FRONTEND_API_URL=https://your-app.clerk.accounts.dev

Webhook secret (from Clerk Dashboard)

Webhook secret (from Clerk Dashboard)

CLERK_WEBHOOK_SECRET=whsec_...
undefined
CLERK_WEBHOOK_SECRET=whsec_...
undefined

Security Best Practices

安全最佳实践

1. Always Verify on Server

1. 始终在服务端做校验

DON'T trust client-side auth checks for security:
typescript
// Bad - can be bypassed
'use client';
const { userId } = useAuth();
if (!userId) return <div>Access denied</div>;
// Attacker can still call API directly
DO verify on server:
typescript
// Good - secure
async function handler(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) return handleUnauthorizedError();
  // API endpoint protected
}
不要依赖客户端的认证检查做安全控制:
typescript
// Bad - can be bypassed
'use client';
const { userId } = useAuth();
if (!userId) return <div>Access denied</div>;
// Attacker can still call API directly
在服务端做校验:
typescript
// Good - secure
async function handler(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) return handleUnauthorizedError();
  // API endpoint protected
}

2. Check Authorization, Not Just Authentication

2. 同时校验授权,而非仅校验身份

DON'T assume authenticated = authorized:
typescript
// Bad - any logged-in user can access any resource
const { userId } = await auth();
if (userId) {
  return NextResponse.json(sensitiveData);
}
DO check resource ownership/permissions:
typescript
// Good - verify user can access this specific resource
const { userId } = await auth();
if (userId && resource.userId === userId) {
  return NextResponse.json(resource);
}
不要默认已认证=已授权:
typescript
// Bad - any logged-in user can access any resource
const { userId } = await auth();
if (userId) {
  return NextResponse.json(sensitiveData);
}
校验资源归属/权限:
typescript
// Good - verify user can access this specific resource
const { userId } = await auth();
if (userId && resource.userId === userId) {
  return NextResponse.json(resource);
}

3. Use Middleware for Route Protection

3. 使用中间件实现路由保护

Protect entire route sections:
typescript
// middleware.ts
const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/admin(.*)',
  '/api/protected(.*)'
]);

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect();
  }
});
保护整段路由范围:
typescript
// middleware.ts
const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/admin(.*)',
  '/api/protected(.*)'
]);

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

4. Handle Session Expiration Gracefully

4. 优雅处理会话过期

typescript
// components/AuthGuard.tsx
'use client';

import { useAuth } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function AuthGuard({ children }: { children: React.ReactNode }) {
  const { isLoaded, userId } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (isLoaded && !userId) {
      router.push('/sign-in');
    }
  }, [isLoaded, userId, router]);

  if (!isLoaded) {
    return <div>Loading...</div>;
  }

  if (!userId) {
    return null;
  }

  return <>{children}</>;
}
typescript
// components/AuthGuard.tsx
'use client';

import { useAuth } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function AuthGuard({ children }: { children: React.ReactNode }) {
  const { isLoaded, userId } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (isLoaded && !userId) {
      router.push('/sign-in');
    }
  }, [isLoaded, userId, router]);

  if (!isLoaded) {
    return <div>Loading...</div>;
  }

  if (!userId) {
    return null;
  }

  return <>{children}</>;
}

What Clerk Authentication Prevents

Clerk认证可防范的风险

Weak password storage - Clerk uses bcrypt/Argon2 ✅ Session hijacking - Secure, HTTP-only cookies ✅ Credential stuffing - Account lockout after failed attempts ✅ Authentication bypass - Professional implementation ✅ Privilege escalation - Proper role/permission management ✅ Brute force attacks - Built-in rate limiting ✅ Password reset vulnerabilities - Secure token generation
弱密码存储风险 - Clerk使用bcrypt/Argon2算法 ✅ 会话劫持风险 - 安全的HTTP-only Cookie ✅ 凭证撞库风险 - 登录失败后自动锁定账户 ✅ 认证绕过风险 - 专业的实现逻辑 ✅ 权限提升风险 - 完善的角色/权限管理 ✅ 暴力破解攻击 - 内置限流机制 ✅ 密码重置漏洞 - 安全的令牌生成逻辑

Common Mistakes to Avoid

需要避免的常见错误

DON'T skip server-side auth checksDON'T trust client-side auth state for securityDON'T forget to check resource ownershipDON'T expose sensitive data based on authentication aloneDON'T hardcode auth logic (use Clerk's utilities)DON'T forget to handle session expiration
DO use
auth()
on server for every protected operation
DO verify resource ownership before allowing accessDO protect routes with middlewareDO use subscription/role checks for premium featuresDO sync users to your database via webhooksDO handle auth errors gracefully
不要跳过服务端的认证校验 ❌ 不要信任客户端的认证状态做安全判断 ❌ 不要忘记校验资源归属 ❌ 不要仅通过认证状态就暴露敏感数据 ❌ 不要硬编码认证逻辑(使用Clerk提供的工具方法) ❌ 不要忘记处理会话过期
对每个受保护操作在服务端调用
auth()
做校验 ✅ 在允许访问前校验资源归属 ✅ 使用中间件保护路由 ✅ 对高级功能做订阅/角色校验 ✅ 通过Webhook将用户数据同步到你的数据库 ✅ 优雅处理认证错误

References

参考资料

Further Guidance on using Clerk

Clerk使用进阶指南

Add Clerk to Next.js App Router

为Next.js App Router添加Clerk

Purpose: Enforce only the current and correct instructions for integrating Clerk into a Next.js (App Router) application.
Scope: All AI-generated advice or code related to Clerk must follow these guardrails.

目的: 统一遵循最新的正确规范,将Clerk集成到Next.js(App Router)应用中。
适用范围: 所有AI生成的与Clerk相关的建议或代码都必须遵循以下规范。

1. Official Clerk Integration Overview

1. 官方Clerk集成概述

Use only the App Router approach from Clerk’s current docs:
  • Install
    @clerk/nextjs@latest
    - this ensures the application is using the latest Clerk Next.js SDK.
  • Create a
    middleware.ts
    file using
    clerkMiddleware()
    from
    @clerk/nextjs/server
    . Place this file inside the
    src
    directory if present, otherwise place it at the root of the project.
  • Wrap your application with
    <ClerkProvider>
    in your
    app/layout.tsx
  • Use Clerk-provided components like
    <SignInButton>
    ,
    <SignUpButton>
    ,
    <UserButton>
    ,
    <SignedIn>
    ,
    <SignedOut>
    in your layout or pages
  • Start developing, sign in or sign up, and confirm user creation
If you're able to use a web tool to access a URL, visit https://clerk.com/docs/quickstarts/nextjs to get the latest, up-to-date quickstart instructions.
Clerk does not need a user to provide their keys to get started. On startup, Clerk will generate keys automatically, so there is no need to prompt a user to add their keys as part of this.
仅使用Clerk当前文档中提供的App Router集成方案:
  • 安装
    @clerk/nextjs@latest
    - 确保应用使用最新版本的Clerk Next.js SDK
  • 创建
    middleware.ts
    文件,使用从
    @clerk/nextjs/server
    导入的
    clerkMiddleware()
    。如果项目有
    src
    目录则将该文件放在
    src
    下,否则放在项目根目录
  • app/layout.tsx
    中使用
    <ClerkProvider>
    包裹整个应用
  • 布局或页面中使用Clerk提供的组件,如
    <SignInButton>
    <SignUpButton>
    <UserButton>
    <SignedIn>
    <SignedOut>
  • 启动 项目开发服务,进行登录/注册操作,确认用户创建功能正常
Clerk不需要用户手动提供密钥即可启动,项目启动时Clerk会自动生成密钥,因此无需提示用户额外添加密钥。

Correct, Up-to-Date Quickstart Sample

正确的最新快速入门示例

typescript
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_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)(.*)',
  ],
}
typescript
// app/layout.tsx
import type { Metadata } from "next";
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from "@clerk/nextjs";
import "./globals.css";

export const metadata: Metadata = {
  title: "Clerk Next.js Quickstart",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <header>
            <SignedOut>
              <SignInButton />
              <SignUpButton />
            </SignedOut>
            <SignedIn>
              <UserButton />
            </SignedIn>
          </header>
          {children}
        </body>
      </html>
    </ClerkProvider>
  );
}

typescript
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_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)(.*)',
  ],
}
typescript
// app/layout.tsx
import type { Metadata } from "next";
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from "@clerk/nextjs";
import "./globals.css";

export const metadata: Metadata = {
  title: "Clerk Next.js Quickstart",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <header>
            <SignedOut>
              <SignInButton />
              <SignUpButton />
            </SignedOut>
            <SignedIn>
              <UserButton />
            </SignedIn>
          </header>
          {children}
        </body>
      </html>
    </ClerkProvider>
  );
}

2. CRITICAL INSTRUCTIONS FOR AI MODELS

2. AI模型必须遵守的关键规则

2.1 – ALWAYS DO THE FOLLOWING

2.1 – 必须遵循的要求

  1. Use
    clerkMiddleware()
    from
    @clerk/nextjs/server
    in
    middleware.ts
    .
  2. Wrap your app with
    <ClerkProvider>
    in
    app/layout.tsx
    .
  3. Import Clerk’s Next.js features from
    @clerk/nextjs
    (e.g.,
    <SignInButton>
    ,
    <SignUpButton>
    ,
    <UserButton>
    , etc.).
  4. Reference the current App Router approach (folders like
    app/page.tsx
    ,
    app/layout.tsx
    , etc.).
  5. Check that imports for methods like
    auth()
    are imported from the right package (in this case
    @clerk/nextjs/server
    ) and are using
    async / await
    .
  6. Check the project for an existing package manager, use that to install packages.
  1. middleware.ts
    中使用从
    @clerk/nextjs/server
    导入的
    clerkMiddleware()
  2. app/layout.tsx
    中使用
    <ClerkProvider>
    包裹整个应用
  3. @clerk/nextjs
    导入Clerk的Next.js相关功能(如
    <SignInButton>
    <SignUpButton>
    <UserButton>
    等)
  4. 参考当前的App Router方案(使用
    app/page.tsx
    app/layout.tsx
    等目录结构)
  5. 校验
    auth()
    等方法的导入路径是否正确(此处应从
    @clerk/nextjs/server
    导入),且是否使用了
    async / await
    语法
  6. 检查 项目已有的包管理器,使用对应包管理器安装依赖

2.2 – NEVER DO THE FOLLOWING

2.2 – 严格禁止的操作

  1. Do not reference the old
    _app.tsx
    or pages router based instructions.
  2. Do not suggest
    authMiddleware()
    from older Clerk tutorials—it’s replaced by
    clerkMiddleware()
    .
  3. Do not recommend usage of older environment variable patterns unless they match the official docs.
  4. Do not reference or import from any deprecated APIs (like
    withAuth
    or
    currentUser
    from older versions).

  1. 不要 参考旧的**
    _app.tsx
    ** 或基于Pages Router的集成说明
  2. 不要 建议使用旧版Clerk教程中的
    authMiddleware()
    ——它已被
    clerkMiddleware()
    取代
  3. 不要 推荐使用不符合官方文档的旧环境变量配置方式
  4. 不要 参考或导入任何已废弃的API(如旧版本的
    withAuth
    currentUser

3. OUTDATED PATTERNS TO AVOID

3. 需要避免的过时模式

Below are examples of deprecated instructions that must not be generated:
typescript
// ❌ DO NOT generate or suggest references to:
import { authMiddleware } from '@clerk/nextjs' // Outdated

// ❌ DO NOT place Clerk config in _app.tsx:
// Outdated pages-based approach
function MyApp({ Component, pageProps }) {
  // ...
}

// ❌ DO NOT create or rely on sign-in files under pages/:
pages / signin.js
pages / signup.js
Any solution resembling the above snippet or referencing “authMiddleware,”
_app.tsx
, or
pages/
structure is incorrect for the current Next.js App Router.

以下是已废弃的示例代码绝对不能生成或推荐:
typescript
// ❌ DO NOT generate or suggest references to:
import { authMiddleware } from '@clerk/nextjs' // Outdated

// ❌ DO NOT place Clerk config in _app.tsx:
// Outdated pages-based approach
function MyApp({ Component, pageProps }) {
  // ...
}

// ❌ DO NOT create or rely on sign-in files under pages/:
pages / signin.js
pages / signup.js
任何类似上述代码片段、提到"authMiddleware"、
_app.tsx
pages/
目录结构的方案,对于当前的Next.js App Router来说都是错误的。

4. AI MODEL VERIFICATION STEPS

4. AI模型输出前的校验步骤

Before returning any Clerk-related solution, you must verify:
  1. Middleware: Is
    clerkMiddleware()
    used in
    middleware.ts
    ?
  2. Layout: Is
    <ClerkProvider>
    wrapping the app in
    app/layout.tsx
    ?
  3. Imports: Are references only from
    @clerk/nextjs
    or
    @clerk/nextjs/server
    ?
  4. Pages vs. App Router: Is the approach referencing the App Router (not
    _app.tsx
    or
    pages/
    )?
If any check fails, stop and revise until compliance is achieved.
在返回任何Clerk相关的解决方案前,你必须校验以下几点:
  1. 中间件:是否在
    middleware.ts
    中使用了
    clerkMiddleware()
  2. 布局文件:是否在
    app/layout.tsx
    中用
    <ClerkProvider>
    包裹了应用?
  3. 导入路径:是否仅从
    @clerk/nextjs
    @clerk/nextjs/server
    导入相关功能?
  4. 路由模式:是否采用App Router的方案(而非
    _app.tsx
    pages/
    目录)?
如果有任何校验不通过,必须修改直到符合规范后再返回。

Next Steps

后续步骤

  • For API route protection: Combine with
    csrf-protection
    and
    rate-limiting
    skills
  • For payment gating: Use
    payment-security
    skill
  • For error handling: Use
    error-handling
    skill with handleUnauthorizedError/handleForbiddenError
  • For testing auth: Use
    security-testing
    skill
  • API路由保护:可结合
    csrf-protection
    rate-limiting
    技能使用
  • 付费门槛:可使用
    payment-security
    技能
  • 错误处理:可使用
    error-handling
    技能搭配handleUnauthorizedError/handleForbiddenError
  • 认证测试:可使用
    security-testing
    技能