mcpField
A
authentication-authorization-clerk

authentication-authorization-clerk

github.com/harperaa安全19186 次安装45 周前更新
代码仓库

关于 authentication-authorization-clerk

authentication-authorization-clerk 是面向 Next.js App Router 应用的 Clerk 安全技能,提供身份验证指导和示例,涵盖登录、路由保护、所有权与角色检查、元数据及用户同步。

主要功能

  1. 01Next.js 的 Clerk 身份验证模式
  2. 02基于中间件的路由保护
  3. 03资源所有权与角色检查
  4. 04用户元数据和自定义声明
  5. 05基于 Webhook 的用户同步

适用场景

  1. 01保护 dashboard 和 API 路由
  2. 02检查用户对自有资源的访问权限
  3. 03按角色或订阅限制功能
  4. 04将 Clerk 用户同步到应用数据库

安装

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

$npx skills add https://github.com/harperaa/secure-claude-skills --skill authentication-authorization-clerk

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

附带文件secure-vibe-coding-OS.code-workspace

详细介绍

使用 Clerk 进行身份验证与授权

为什么使用 Clerk

身份验证难题

从头构建安全的身份验证需要:

  • 密码哈希(使用适当盐值的 bcrypt/Argon2)
  • 会话管理(安全 Cookie、过期时间、续期)
  • 密码重置流程(安全令牌生成、电子邮件验证)
  • 账户锁定(防止暴力破解)
  • MFA 支持(TOTP、短信、身份验证器应用)
  • 社交登录(Google、GitHub 等的 OAuth 流程)
  • 用户数据库同步
  • 上述所有功能的安全最佳实践

安全实现所需时间: 经验丰富的开发者需要 2-4 周

对于使用 AI 的 vibe coder: 安全漏洞风险较高

现实世界中的自定义身份验证故障

Ashley Madison 数据泄露事件(2015): 自定义身份验证采用了薄弱的密码哈希方式。3,200 万个账户遭到泄露。

Dropbox 数据泄露事件(2012): 自定义身份验证导致密码哈希数据库被盗。6,800 万个账户受到影响。

根据 Veracode 的 2024 年报告,使用托管身份验证服务(如 Clerk、Auth0)的应用,其身份验证相关漏洞比使用自定义身份验证的应用少 73%。

我们的 Clerk 架构

Clerk 处理的事项(因此我们无需处理)
  • ✅ 密码哈希(bcrypt/Argon2)
  • ✅ 会话管理(安全 Cookie)
  • ✅ MFA(内置支持)
  • ✅ OAuth 提供商(Google、GitHub 等)
  • ✅ 电子邮件验证
  • ✅ 密码重置流程
  • ✅ 账户锁定
  • ✅ 安全监控
  • ✅ 合规性(SOC 2、GDPR)

Clerk 已通过 SOC 2 认证: 这意味着独立审计员已验证其安全控制符合行业标准。我们也因此享有这项认证。

实现文件

  • middleware.ts - 用于受保护路由的 Clerk 身份验证
  • app/dashboard/* - 由中间件保护
  • Clerk 管理自己的会话 Cookie

基本身份验证

服务端身份验证(API 路由)
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
}
客户端身份验证(组件)
'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>
  );
}
使用中间件保护路由
// 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)(.*)'],
};

授权模式

验证资源所有权
// 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 });
}
基于角色的访问控制(RBAC)
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
}
基于订阅的授权
服务端(API 路由)
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
}
客户端(组件)
'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>
  );
}

完整的受保护 API 路由示例

// 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',
};

用户元数据与自定义声明

存储用户元数据

Clerk 支持为每位用户存储自定义元数据:

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
    }
  });
}
访问元数据

服务端:

const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.plan;

客户端:

const { user } = useUser();
const plan = user?.publicMetadata?.plan;

Webhook 集成(用户同步)

当用户注册或更新个人资料时,将其同步到数据库:

// 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 集成

在 Convex 中使用 Clerk 身份验证
// 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();
  }
});

登录/注册组件

基本登录页面
// 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>
  );
}
基本注册页面
// 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>
  );
}
用户按钮(个人资料/退出登录)
// 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>
  );
}

环境配置

必需的环境变量
# .env.local

# Clerk (from Clerk Dashboard)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

# 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

# Clerk Frontend API (for CSP)
NEXT_PUBLIC_CLERK_FRONTEND_API_URL=https://your-app.clerk.accounts.dev

# Webhook secret (from Clerk Dashboard)
CLERK_WEBHOOK_SECRET=whsec_...

安全最佳实践

1. 始终在服务端验证

❌ 不要将客户端身份验证检查视为安全保障:

// Bad - can be bypassed
'use client';
const { userId } = useAuth();
if (!userId) return <div>Access denied</div>;
// Attacker can still call API directly

✅ 务必在服务端验证:

// Good - secure
async function handler(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) return handleUnauthorizedError();
  // API endpoint protected
}
2. 检查授权,而不仅仅是身份验证

❌ 不要假设已通过身份验证就等于已获授权:

// Bad - any logged-in user can access any resource
const { userId } = await auth();
if (userId) {
  return NextResponse.json(sensitiveData);
}

✅ 务必检查资源所有权/权限:

// Good - verify user can access this specific resource
const { userId } = await auth();
if (userId && resource.userId === userId) {
  return NextResponse.json(resource);
}
3. 使用中间件保护路由

✅ 保护整个路由分区:

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

export default clerkMiddleware((auth, req) => {
  if (isProtectedRoute(req)) {
    auth().protect();
  }
});
4. 妥善处理会话过期
// 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}</>;
}

Clerk 身份验证可防范的问题

✅ 弱密码存储 - Clerk 使用 bcrypt/Argon2
✅ 会话劫持 - 使用安全的 HTTP-only Cookie
✅ 凭据填充攻击 - 多次尝试失败后锁定账户
✅ 绕过身份验证 - 由专业团队实现
✅ 权限提升 - 正确管理角色/权限
✅ 暴力破解攻击 - 内置速率限制
✅ 密码重置漏洞 - 安全的令牌生成

常见错误,请避免

❌ 不要跳过服务端身份验证检查 ❌ 不要将客户端身份验证状态视为安全依据 ❌ 不要忘记检查资源所有权 ❌ 不要仅凭身份验证就公开敏感数据 ❌ 不要硬编码身份验证逻辑(请使用 Clerk 的实用工具) ❌ 不要忘记处理会话过期

✅ 每次执行受保护的操作时,都要在服务器端使用 auth() ✅ 允许访问前,务必验证资源所有权 ✅ 使用中间件保护路由 ✅ 高级功能要使用订阅/角色检查 ✅ 通过 Webhook 将用户同步到数据库 ✅ 妥善处理身份验证错误

参考资料

使用 Clerk 的更多指南

将 Clerk 添加到 Next.js App Router

目的:仅采用将 Clerk 集成到 Next.js(App Router)应用程序中的当前且正确的说明。
**范围:**所有与 Clerk 相关的 AI 生成建议或代码都必须遵循这些规范。


1. Clerk 官方集成概览

仅使用 Clerk 当前文档中的 App Router 方案:

  • 安装 @clerk/nextjs@latest ——确保应用程序使用最新的 Clerk Next.js SDK。
  • 创建一个 middleware.ts 文件,并使用来自 @clerk/nextjs/server 的 clerkMiddleware()。如果存在 src 目录,请将此文件放在其中;否则放在项目根目录。
  • 在 app/layout.tsx 中使用 <ClerkProvider> 包装应用程序
  • 在布局或页面中使用 Clerk 提供的组件,例如 <SignInButton>、<SignUpButton>、<UserButton>、<SignedIn>、<SignedOut>
  • 开始开发,登录或注册,并确认用户已创建

如果你可以使用 Web 工具访问 URL,请前往 https://clerk.com/docs/quickstarts/nextjs 获取最新的快速入门说明。

开始使用 Clerk 无需用户提供密钥。启动时,Clerk 会自动生成密钥,因此无需提示用户在此过程中添加密钥。

正确且最新的快速入门示例
// 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)(.*)',
  ],
}
// 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. 给 AI 模型的重要指示

2.1 – 始终执行以下操作
  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 – 绝不要执行以下操作
  1. 不要引用旧版 _app.tsx 或基于 pages router 的说明。
  2. 不要建议使用旧版 Clerk 教程中的 authMiddleware()——它已被 clerkMiddleware() 替代。
  3. 除非符合官方文档,否则不要推荐使用旧版环境变量模式。
  4. 不要引用或导入任何已弃用的 API(例如旧版本中的 withAuth 或 currentUser)。

3. 应避免的过时模式

以下是不得生成的已弃用说明示例:

// ❌ 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 模型验证步骤

返回任何与 Clerk 相关的解决方案前,必须验证:

  1. 中间件:middleware.ts 中是否使用了 clerkMiddleware()?
  2. 布局:app/layout.tsx 中是否由 <ClerkProvider> 包裹应用?
  3. 导入:引用是否仅来自 @clerk/nextjs 或 @clerk/nextjs/server?
  4. 页面与 App Router:采用的方法是否引用了 App Router(而非 _app.tsx 或 pages/)?

如果任何检查未通过,请停止并修订,直到符合要求。

后续步骤

  • 保护 API 路由:与 csrf-protection 和 rate-limiting 技能结合使用
  • 限制支付功能:使用 payment-security 技能
  • 处理错误:使用 error-handling 技能以及 handleUnauthorizedError/handleForbiddenError
  • 测试身份验证:使用 security-testing 技能

评论

支持 Markdown