clerk-authentication
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClerk Authentication
Clerk 认证
You are an expert in Clerk authentication implementation for Next.js applications. Follow these guidelines when integrating Clerk.
您是Next.js应用中Clerk认证实现的专家。集成Clerk时请遵循以下指南。
Core Principles
核心原则
- Implement defense-in-depth with multiple authentication layers
- Verify authentication at every data access point, not just middleware
- Protect server actions individually
- Use Clerk's built-in security features (HttpOnly cookies, CSRF protection)
- 实现多层认证的纵深防御
- 在每个数据访问点验证认证,而不仅仅是中间件
- 单独保护每个Server Action
- 使用Clerk的内置安全功能(HttpOnly Cookie、CSRF防护)
Installation and Setup
安装与配置
bash
npm install @clerk/nextjsbash
npm install @clerk/nextjsEnvironment Variables
环境变量
bash
undefinedbash
undefinedRequired
必填
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
Optional: Custom URLs
可选:自定义URL
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
undefinedNEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
undefinedProvider Setup
Provider配置
App Router (app/layout.tsx)
App Router(app/layout.tsx)
typescript
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}typescript
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}With Custom Appearance
自定义外观配置
typescript
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
},
elements: {
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
},
}}
>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}typescript
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
},
elements: {
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
},
}}
>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}Middleware Configuration
中间件配置
Basic Middleware (middleware.ts)
基础中间件(middleware.ts)
typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// Define protected routes
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/protected(.*)',
'/settings(.*)',
]);
// Define public routes (optional, for clarity)
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/public(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
// Skip Next.js internals and static files
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
};typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
// 定义受保护路由
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/protected(.*)',
'/settings(.*)',
]);
// 定义公开路由(可选,用于清晰区分)
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/public(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
// 跳过Next.js内部文件和静态文件
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// 始终对API路由生效
'/(api|trpc)(.*)',
],
};Advanced Middleware with Role-Based Access
基于角色访问控制的高级中间件
typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/protected(.*)']);
export default clerkMiddleware(async (auth, req) => {
const { userId, sessionClaims } = await auth();
// Admin routes require admin role
if (isAdminRoute(req)) {
if (!userId || sessionClaims?.metadata?.role !== 'admin') {
return new Response('Forbidden', { status: 403 });
}
}
// Protected routes require authentication
if (isProtectedRoute(req)) {
await auth.protect();
}
});typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/protected(.*)']);
export default clerkMiddleware(async (auth, req) => {
const { userId, sessionClaims } = await auth();
// 管理员路由需要admin角色
if (isAdminRoute(req)) {
if (!userId || sessionClaims?.metadata?.role !== 'admin') {
return new Response('Forbidden', { status: 403 });
}
}
// 受保护路由需要认证
if (isProtectedRoute(req)) {
await auth.protect();
}
});Authentication in Server Components
Server Components中的认证
Using auth()
使用auth()
typescript
import { auth } from '@clerk/nextjs/server';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
// Fetch user-specific data
const data = await fetchUserData(userId);
return <Dashboard data={data} />;
}typescript
import { auth } from '@clerk/nextjs/server';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
// 获取用户专属数据
const data = await fetchUserData(userId);
return <Dashboard data={data} />;
}Using currentUser()
使用currentUser()
typescript
import { currentUser } from '@clerk/nextjs/server';
export default async function ProfilePage() {
const user = await currentUser();
if (!user) {
redirect('/sign-in');
}
return (
<div>
<h1>Welcome, {user.firstName}!</h1>
<p>Email: {user.emailAddresses[0]?.emailAddress}</p>
</div>
);
}typescript
import { currentUser } from '@clerk/nextjs/server';
export default async function ProfilePage() {
const user = await currentUser();
if (!user) {
redirect('/sign-in');
}
return (
<div>
<h1>欢迎,{user.firstName}!</h1>
<p>邮箱:{user.emailAddresses[0]?.emailAddress}</p>
</div>
);
}Authentication in Client Components
Client Components中的认证
useUser Hook
useUser钩子
typescript
'use client';
import { useUser } from '@clerk/nextjs';
export function UserProfile() {
const { isLoaded, isSignedIn, user } = useUser();
if (!isLoaded) {
return <Skeleton />;
}
if (!isSignedIn) {
return <SignInPrompt />;
}
return (
<div>
<img src={user.imageUrl} alt={user.fullName ?? 'User'} />
<p>{user.fullName}</p>
</div>
);
}typescript
'use client';
import { useUser } from '@clerk/nextjs';
export function UserProfile() {
const { isLoaded, isSignedIn, user } = useUser();
if (!isLoaded) {
return <Skeleton />;
}
if (!isSignedIn) {
return <SignInPrompt />;
}
return (
<div>
<img src={user.imageUrl} alt={user.fullName ?? 'User'} />
<p>{user.fullName}</p>
</div>
);
}useAuth Hook
useAuth钩子
typescript
'use client';
import { useAuth } from '@clerk/nextjs';
export function ProtectedAction() {
const { isLoaded, userId, getToken } = useAuth();
async function handleAction() {
if (!userId) return;
// Get a fresh token for API calls
const token = await getToken();
const response = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
if (!isLoaded || !userId) {
return null;
}
return <button onClick={handleAction}>Perform Action</button>;
}typescript
'use client';
import { useAuth } from '@clerk/nextjs';
export function ProtectedAction() {
const { isLoaded, userId, getToken } = useAuth();
async function handleAction() {
if (!userId) return;
// 获取用于API调用的新令牌
const token = await getToken();
const response = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
if (!isLoaded || !userId) {
return null;
}
return <button onClick={handleAction}>执行操作</button>;
}Server Actions Protection
Server Action保护
Always protect server actions individually:
typescript
'use server';
import { auth } from '@clerk/nextjs/server';
export async function createPost(formData: FormData) {
const { userId } = await auth();
if (!userId) {
throw new Error('Unauthorized');
}
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Create post with user ID
const post = await db.post.create({
data: {
title,
content,
authorId: userId,
},
});
revalidatePath('/posts');
return post;
}始终单独保护每个Server Action:
typescript
'use server';
import { auth } from '@clerk/nextjs/server';
export async function createPost(formData: FormData) {
const { userId } = await auth();
if (!userId) {
throw new Error('未授权');
}
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// 使用用户ID创建帖子
const post = await db.post.create({
data: {
title,
content,
authorId: userId,
},
});
revalidatePath('/posts');
return post;
}With Role Validation
带角色验证
typescript
'use server';
import { auth } from '@clerk/nextjs/server';
export async function deleteUser(userId: string) {
const { userId: currentUserId, sessionClaims } = await auth();
if (!currentUserId) {
throw new Error('Unauthorized');
}
if (sessionClaims?.metadata?.role !== 'admin') {
throw new Error('Forbidden: Admin access required');
}
await db.user.delete({ where: { id: userId } });
revalidatePath('/admin/users');
}typescript
'use server';
import { auth } from '@clerk/nextjs/server';
export async function deleteUser(userId: string) {
const { userId: currentUserId, sessionClaims } = await auth();
if (!currentUserId) {
throw new Error('未授权');
}
if (sessionClaims?.metadata?.role !== 'admin') {
throw new Error('禁止访问:需要管理员权限');
}
await db.user.delete({ where: { id: userId } });
revalidatePath('/admin/users');
}API Route Protection
API路由保护
Route Handlers (App Router)
路由处理器(App Router)
typescript
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const data = await fetchUserData(userId);
return NextResponse.json(data);
}
export async function POST(request: Request) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
// Process request...
return NextResponse.json({ success: true });
}typescript
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const data = await fetchUserData(userId);
return NextResponse.json(data);
}
export async function POST(request: Request) {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
const body = await request.json();
// 处理请求...
return NextResponse.json({ success: true });
}JWT Verification for External APIs
外部API的JWT验证
typescript
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { getToken } = await auth();
// Get JWT for external API
const token = await getToken({ template: 'external-api' });
const response = await fetch('https://external-api.com/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});
return Response.json(await response.json());
}typescript
import { auth } from '@clerk/nextjs/server';
export async function GET() {
const { getToken } = await auth();
// 获取用于外部API的JWT
const token = await getToken({ template: 'external-api' });
const response = await fetch('https://external-api.com/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});
return Response.json(await response.json());
}Organization Support
组织支持
typescript
import { auth } from '@clerk/nextjs/server';
export async function getOrganizationData() {
const { userId, orgId, orgRole } = await auth();
if (!userId || !orgId) {
throw new Error('Must be in an organization');
}
// Check organization role
if (orgRole !== 'org:admin') {
throw new Error('Admin access required');
}
return await db.organization.findUnique({
where: { clerkOrgId: orgId },
});
}typescript
import { auth } from '@clerk/nextjs/server';
export async function getOrganizationData() {
const { userId, orgId, orgRole } = await auth();
if (!userId || !orgId) {
throw new Error('必须加入组织');
}
// 检查组织角色
if (orgRole !== 'org:admin') {
throw new Error('需要管理员权限');
}
return await db.organization.findUnique({
where: { clerkOrgId: orgId },
});
}Custom Session Claims
自定义会话声明
Configure in Clerk Dashboard
在Clerk控制台配置
Add custom claims via JWT Templates, then access them:
typescript
import { auth } from '@clerk/nextjs/server';
export async function checkSubscription() {
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.subscriptionPlan;
if (plan !== 'pro') {
throw new Error('Pro subscription required');
}
}通过JWT模板添加自定义声明,然后访问:
typescript
import { auth } from '@clerk/nextjs/server';
export async function checkSubscription() {
const { sessionClaims } = await auth();
const plan = sessionClaims?.metadata?.subscriptionPlan;
if (plan !== 'pro') {
throw new Error('需要Pro订阅');
}
}UI Components
UI组件
Pre-built Components
预构建组件
typescript
import {
SignIn,
SignUp,
SignOutButton,
UserButton,
SignedIn,
SignedOut,
} from '@clerk/nextjs';
export function Header() {
return (
<header>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
</header>
);
}
// Dedicated sign-in page
export default function SignInPage() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignIn />
</div>
);
}typescript
import {
SignIn,
SignUp,
SignOutButton,
UserButton,
SignedIn,
SignedOut,
} from '@clerk/nextjs';
export function Header() {
return (
<header>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
</header>
);
}
// 独立登录页
export default function SignInPage() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignIn />
</div>
);
}Security Best Practices
安全最佳实践
1. Defense in Depth
1. 纵深防御
typescript
// Layer 1: Middleware
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
// Layer 2: Server Component
export default async function Page() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
// ...
}
// Layer 3: Data Access
async function fetchUserData(userId: string) {
const { userId: currentUserId } = await auth();
if (currentUserId !== userId) throw new Error('Forbidden');
// ...
}typescript
// 第一层:中间件
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
// 第二层:Server Component
export default async function Page() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
// ...
}
// 第三层:数据访问
async function fetchUserData(userId: string) {
const { userId: currentUserId } = await auth();
if (currentUserId !== userId) throw new Error('禁止访问');
// ...
}2. Protect All Server Actions
2. 保护所有Server Action
typescript
// Every server action should verify auth independently
'use server';
export async function sensitiveAction() {
const { userId } = await auth();
if (!userId) throw new Error('Unauthorized');
// ...
}typescript
// 每个Server Action都应独立验证认证
'use server';
export async function sensitiveAction() {
const { userId } = await auth();
if (!userId) throw new Error('未授权');
// ...
}3. Avoid Client-Side Only Protection
3. 避免仅依赖客户端保护
typescript
// BAD: Client-side only check
'use client';
export function SecretComponent() {
const { isSignedIn } = useAuth();
if (!isSignedIn) return null;
return <div>Secret Data</div>; // Data still sent to client!
}
// GOOD: Server-side protection
export default async function SecretPage() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
const data = await fetchSecretData(userId);
return <SecretComponent data={data} />;
}typescript
// 错误示例:仅客户端检查
'use client';
export function SecretComponent() {
const { isSignedIn } = useAuth();
if (!isSignedIn) return null;
return <div>敏感数据</div>; // 数据仍会发送到客户端!
}
// 正确示例:服务端保护
export default async function SecretPage() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
const data = await fetchSecretData(userId);
return <SecretComponent data={data} />;
}Error Handling
错误处理
typescript
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function ProtectedPage() {
try {
const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
const data = await fetchUserData(userId);
return <Dashboard data={data} />;
} catch (error) {
if (error instanceof AuthenticationError) {
redirect('/sign-in');
}
throw error;
}
}typescript
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function ProtectedPage() {
try {
const { userId } = await auth();
if (!userId) {
redirect('/sign-in');
}
const data = await fetchUserData(userId);
return <Dashboard data={data} />;
} catch (error) {
if (error instanceof AuthenticationError) {
redirect('/sign-in');
}
throw error;
}
}Testing
测试
typescript
// Mock Clerk for testing
import { auth } from '@clerk/nextjs/server';
jest.mock('@clerk/nextjs/server', () => ({
auth: jest.fn(),
}));
describe('Protected API', () => {
it('returns 401 for unauthenticated requests', async () => {
(auth as jest.Mock).mockResolvedValue({ userId: null });
const response = await GET();
expect(response.status).toBe(401);
});
it('returns data for authenticated requests', async () => {
(auth as jest.Mock).mockResolvedValue({ userId: 'user_123' });
const response = await GET();
expect(response.status).toBe(200);
});
});typescript
// 测试时Mock Clerk
import { auth } from '@clerk/nextjs/server';
jest.mock('@clerk/nextjs/server', () => ({
auth: jest.fn(),
}));
describe('受保护API', () => {
it('对未认证请求返回401', async () => {
(auth as jest.Mock).mockResolvedValue({ userId: null });
const response = await GET();
expect(response.status).toBe(401);
});
it('对已认证请求返回数据', async () => {
(auth as jest.Mock).mockResolvedValue({ userId: 'user_123' });
const response = await GET();
expect(response.status).toBe(200);
});
});Common Anti-Patterns to Avoid
需要避免的常见反模式
- Relying solely on middleware for protection
- Not protecting server actions individually
- Using client-side auth checks for sensitive data
- Exposing user data without ownership verification
- Not validating organization membership for org-scoped resources
- Hardcoding role checks instead of using Clerk's RBAC
- Not handling loading states in client components
- 仅依赖中间件进行保护
- 未单独保护每个Server Action
- 对敏感数据仅使用客户端认证检查
- 在未验证所有权的情况下暴露用户数据
- 未针对组织范围的资源验证组织成员身份
- 硬编码角色检查而非使用Clerk的RBAC
- 未在客户端组件中处理加载状态