authentication-authorization-clerk
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuthentication & 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
实现文件说明
- - Clerk authentication for protected routes
middleware.ts - - Protected by middleware
app/dashboard/* - Clerk manages its own session cookies
- - 用于受保护路由的Clerk认证逻辑
middleware.ts - - 由中间件保护的路由范围
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
undefinedbash
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_...
undefinedCLERK_WEBHOOK_SECRET=whsec_...
undefinedSecurity 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 checks
❌ DON'T trust client-side auth state for security
❌ DON'T forget to check resource ownership
❌ DON'T expose sensitive data based on authentication alone
❌ DON'T hardcode auth logic (use Clerk's utilities)
❌ DON'T forget to handle session expiration
✅ DO use on server for every protected operation
✅ DO verify resource ownership before allowing access
✅ DO protect routes with middleware
✅ DO use subscription/role checks for premium features
✅ DO sync users to your database via webhooks
✅ DO handle auth errors gracefully
auth()❌ 不要跳过服务端的认证校验
❌ 不要信任客户端的认证状态做安全判断
❌ 不要忘记校验资源归属
❌ 不要仅通过认证状态就暴露敏感数据
❌ 不要硬编码认证逻辑(使用Clerk提供的工具方法)
❌ 不要忘记处理会话过期
✅ 请对每个受保护操作在服务端调用做校验
✅ 请在允许访问前校验资源归属
✅ 请使用中间件保护路由
✅ 请对高级功能做订阅/角色校验
✅ 请通过Webhook将用户数据同步到你的数据库
✅ 请优雅处理认证错误
auth()References
参考资料
- Clerk Documentation: https://clerk.com/docs
- Clerk Security: https://clerk.com/docs/security
- Clerk Next.js Integration: https://clerk.com/docs/references/nextjs/overview
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- 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
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.
Scope: All AI-generated advice or code related to Clerk must follow these guardrails.
目的: 统一遵循最新的正确规范,将Clerk集成到Next.js(App Router)应用中。
适用范围: 所有AI生成的与Clerk相关的建议或代码都必须遵循以下规范。
适用范围: 所有AI生成的与Clerk相关的建议或代码都必须遵循以下规范。
1. Official Clerk Integration Overview
1. 官方Clerk集成概述
Use only the App Router approach from Clerk’s current docs:
- Install - this ensures the application is using the latest Clerk Next.js SDK.
@clerk/nextjs@latest - Create a file using
middleware.tsfromclerkMiddleware(). Place this file inside the@clerk/nextjs/serverdirectory if present, otherwise place it at the root of the project.src - Wrap your application with in your
<ClerkProvider>app/layout.tsx - Use Clerk-provided components like ,
<SignInButton>,<SignUpButton>,<UserButton>,<SignedIn>in your layout or pages<SignedOut> - 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 Next.js SDK
@clerk/nextjs@latest - 创建 文件,使用从
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 – 必须遵循的要求
- Use from
clerkMiddleware()in@clerk/nextjs/server.middleware.ts - Wrap your app with in
<ClerkProvider>.app/layout.tsx - Import Clerk’s Next.js features from (e.g.,
@clerk/nextjs,<SignInButton>,<SignUpButton>, etc.).<UserButton> - Reference the current App Router approach (folders like ,
app/page.tsx, etc.).app/layout.tsx - Check that imports for methods like are imported from the right package (in this case
auth()) and are using@clerk/nextjs/server.async / await - Check the project for an existing package manager, use that to install packages.
- 在 中使用从
middleware.ts导入的@clerk/nextjs/serverclerkMiddleware() - 在 中使用
app/layout.tsx包裹整个应用<ClerkProvider> - 从 导入Clerk的Next.js相关功能(如
@clerk/nextjs、<SignInButton>、<SignUpButton>等)<UserButton> - 仅 参考当前的App Router方案(使用、
app/page.tsx等目录结构)app/layout.tsx - 校验 等方法的导入路径是否正确(此处应从
auth()导入),且是否使用了@clerk/nextjs/server语法async / await - 检查 项目已有的包管理器,使用对应包管理器安装依赖
2.2 – NEVER DO THE FOLLOWING
2.2 – 严格禁止的操作
- Do not reference the old or pages router based instructions.
_app.tsx - Do not suggest from older Clerk tutorials—it’s replaced by
authMiddleware().clerkMiddleware() - Do not recommend usage of older environment variable patterns unless they match the official docs.
- Do not reference or import from any deprecated APIs (like or
withAuthfrom older versions).currentUser
- 不要 参考旧的**** 或基于Pages Router的集成说明
_app.tsx - 不要 建议使用旧版Clerk教程中的——它已被
authMiddleware()取代clerkMiddleware() - 不要 推荐使用不符合官方文档的旧环境变量配置方式
- 不要 参考或导入任何已废弃的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.jsAny solution resembling the above snippet or referencing “authMiddleware,” , or structure is incorrect for the current Next.js App Router.
_app.tsxpages/以下是已废弃的示例代码,绝对不能生成或推荐:
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"、或目录结构的方案,对于当前的Next.js App Router来说都是错误的。
_app.tsxpages/4. AI MODEL VERIFICATION STEPS
4. AI模型输出前的校验步骤
Before returning any Clerk-related solution, you must verify:
- Middleware: Is used in
clerkMiddleware()?middleware.ts - Layout: Is wrapping the app in
<ClerkProvider>?app/layout.tsx - Imports: Are references only from or
@clerk/nextjs?@clerk/nextjs/server - Pages vs. App Router: Is the approach referencing the App Router (not or
_app.tsx)?pages/
If any check fails, stop and revise until compliance is achieved.
在返回任何Clerk相关的解决方案前,你必须校验以下几点:
- 中间件:是否在中使用了
middleware.ts?clerkMiddleware() - 布局文件:是否在中用
app/layout.tsx包裹了应用?<ClerkProvider> - 导入路径:是否仅从或
@clerk/nextjs导入相关功能?@clerk/nextjs/server - 路由模式:是否采用App Router的方案(而非或
_app.tsx目录)?pages/
如果有任何校验不通过,必须修改直到符合规范后再返回。
Next Steps
后续步骤
- For API route protection: Combine with and
csrf-protectionskillsrate-limiting - For payment gating: Use skill
payment-security - For error handling: Use skill with handleUnauthorizedError/handleForbiddenError
error-handling - For testing auth: Use skill
security-testing
- API路由保护:可结合和
csrf-protection技能使用rate-limiting - 付费门槛:可使用技能
payment-security - 错误处理:可使用技能搭配handleUnauthorizedError/handleForbiddenError
error-handling - 认证测试:可使用技能
security-testing