关于 authentication-authorization-clerk
authentication-authorization-clerk 是面向 Next.js App Router 应用的 Clerk 安全技能,提供身份验证指导和示例,涵盖登录、路由保护、所有权与角色检查、元数据及用户同步。
主要功能
- 01Next.js 的 Clerk 身份验证模式
- 02基于中间件的路由保护
- 03资源所有权与角色检查
- 04用户元数据和自定义声明
- 05基于 Webhook 的用户同步
适用场景
- 01保护 dashboard 和 API 路由
- 02检查用户对自有资源的访问权限
- 03按角色或订阅限制功能
- 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 文档:https://clerk.com/docs
- Clerk 安全性:https://clerk.com/docs/security
- Clerk Next.js 集成:https://clerk.com/docs/references/nextjs/overview
- OWASP 身份验证速查表:https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
使用 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 – 始终执行以下操作
- 在
middleware.ts中使用来自@clerk/nextjs/server的clerkMiddleware()。 - 在
app/layout.tsx中使用<ClerkProvider>包装应用。 - 从
@clerk/nextjs导入 Clerk 的 Next.js 功能(例如<SignInButton>、<SignUpButton>、<UserButton>等)。 - 参照当前的 App Router 方案(如
app/page.tsx、app/layout.tsx等目录和文件)。 - 检查
auth()等方法是否从正确的程序包导入(此处为@clerk/nextjs/server),并且是否使用async / await。 - 检查项目中是否已有包管理器,并使用它安装程序包。
2.2 – 绝不要执行以下操作
- 不要引用旧版
_app.tsx或基于 pages router 的说明。 - 不要建议使用旧版 Clerk 教程中的
authMiddleware()——它已被clerkMiddleware()替代。 - 除非符合官方文档,否则不要推荐使用旧版环境变量模式。
- 不要引用或导入任何已弃用的 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 相关的解决方案前,必须验证:
- 中间件:
middleware.ts中是否使用了clerkMiddleware()? - 布局:
app/layout.tsx中是否由<ClerkProvider>包裹应用? - 导入:引用是否仅来自
@clerk/nextjs或@clerk/nextjs/server? - 页面与 App Router:采用的方法是否引用了 App Router(而非
_app.tsx或pages/)?
如果任何检查未通过,请停止并修订,直到符合要求。
后续步骤
- 保护 API 路由:与
csrf-protection和rate-limiting技能结合使用 - 限制支付功能:使用
payment-security技能 - 处理错误:使用
error-handling技能以及 handleUnauthorizedError/handleForbiddenError - 测试身份验证:使用
security-testing技能