Clerk 用户认证系统集成文档
Clerk 用户认证系统集成文档
目录
- 概述
- 安装与配置
- 环境变量配置
- Nuxt配置
- 组件集成
- 状态管理
- 样式定制
- 路由保护
- API集成
- 最佳实践
- 故障排除
概述
Clerk是一个现代化的用户认证和用户管理平台,提供了完整的身份验证解决方案。本项目已集成Clerk,支持多种登录方式:
- ✅ 邮箱密码登录
- ✅ 社交登录 (Google, GitHub, Facebook等)
- ✅ 手机号验证码登录
- ✅ 邮箱验证码登录
- ✅ 多因素认证(MFA)
- ✅ 组织管理
- ✅ 用户资料管理
📦 安装与配置
1. 依赖包安装
项目已安装以下Clerk相关依赖:
{"dependencies": {"@clerk/themes": "^2.4.13","@clerk/nuxt": "^1.8.10", "@clerk/vue": "^1.11.4"}
}
2. 安装命令
# 使用 pnpm
pnpm add @clerk/themes @clerk/nuxt @clerk/vue# 或使用 npm
npm install @clerk/themes @clerk/nuxt @clerk/vue# 或使用 yarn
yarn add @clerk/themes @clerk/nuxt @clerk/vue
🔧 环境变量配置
1. 创建环境变量文件
创建 .env
文件:
# Clerk 配置
NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
NUXT_CLERK_SECRET_KEY=sk_test_your_secret_key_here# 可选:自定义域名
NUXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NUXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NUXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NUXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
2. 获取Clerk密钥
- 访问 Clerk Dashboard
- 创建新应用或选择现有应用
- 在 API Keys 页面获取:
- Publishable Key (前端使用)
- Secret Key (后端使用)
3. 环境变量说明
变量名 | 说明 | 必需 |
---|---|---|
NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY | 前端公钥 | ✅ |
NUXT_CLERK_SECRET_KEY | 后端密钥 | ✅ |
NUXT_PUBLIC_CLERK_SIGN_IN_URL | 登录页面路径 | ❌ |
NUXT_PUBLIC_CLERK_SIGN_UP_URL | 注册页面路径 | ❌ |
NUXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL | 登录后跳转路径 | ❌ |
NUXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL | 注册后跳转路径 | ❌ |
⚙️ Nuxt配置
1. 基础配置
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'
import { dark } from '@clerk/themes'export default defineNuxtConfig({modules: ['@nuxtjs/tailwindcss', '@clerk/nuxt'],clerk: {appearance: {baseTheme: dark,},// 禁用自动服务端中间件,避免全局拦截skipServerMiddleware: true,},runtimeConfig: {public: {clerkPublishableKey: process.env.NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '',},// 服务端配置clerkSecretKey: process.env.NUXT_CLERK_SECRET_KEY || '',},nitro: {routeRules: {// 首页和其他页面不使用 Clerk 自动认证'/': { ssr: true, prerender: false },'/chat/**': { ssr: true, prerender: false },// 确保登录页面支持OAuth回调参数'/login': { ssr: false, prerender: false },},},
})
2. 高级配置选项
clerk: {appearance: {baseTheme: dark,variables: {colorPrimary: '#00d084',colorBackground: 'transparent',colorInputBackground: '#2a2a2a',colorInputText: '#ffffff',colorText: '#ffffff',colorTextSecondary: '#9ca3af',borderRadius: '8px',fontSize: '16px',},},// 自定义路由signInUrl: '/login',signUpUrl: '/sign-up',afterSignInUrl: '/',afterSignUpUrl: '/',// 禁用自动重定向skipServerMiddleware: true,
}
🧩 组件集成
1. 登录页面集成
<!-- src/pages/login/index.tsx -->
<template><div class="login-container"><h1>Sign in</h1><SignIntransferable={true}withSignUp={true}oauthFlow="redirect"appearance={{baseTheme: dark,variables: {colorPrimary: '#00d084',colorBackground: 'transparent',colorInputBackground: '#2a2a2a',colorInputText: '#ffffff',colorText: '#ffffff',colorTextSecondary: '#9ca3af',borderRadius: '8px',fontSize: '16px',},elements: {card: {backgroundColor: 'transparent',boxShadow: 'none',border: 'none',padding: '0',},socialButtonsBlock: {display: 'flex',flexDirection: 'column',gap: '12px',width: '100%',},formButtonPrimary: {backgroundColor: '#00d084',color: '#ffffff',borderRadius: '8px',height: '48px',},},}}/></div>
</template><script setup>
import { SignIn } from '@clerk/vue'
import { dark } from '@clerk/themes'
</script>
2. 注册页面
<!-- src/pages/sign-up/index.tsx -->
<template><div class="signup-container"><h1>Create Account</h1><SignUptransferable={true}withSignIn={true}oauthFlow="redirect"appearance={{baseTheme: dark,// 与登录页面相同的样式配置}}/></div>
</template><script setup>
import { SignUp } from '@clerk/vue'
import { dark } from '@clerk/themes'
</script>
3. 用户按钮组件
<!-- src/components/UserButton.vue -->
<template><UserButtonappearance={{baseTheme: dark,elements: {userButtonAvatarBox: {width: '40px',height: '40px',},userButtonPopoverCard: {backgroundColor: '#2a2a2a',borderColor: '#404040',},},}}/>
</template><script setup>
import { UserButton } from '@clerk/vue'
import { dark } from '@clerk/themes'
</script>
4. 组织管理组件
<!-- src/components/OrganizationSwitcher.vue -->
<template><OrganizationSwitcherappearance={{baseTheme: dark,elements: {organizationSwitcherTrigger: {backgroundColor: '#2a2a2a',borderColor: '#404040',},},}}/>
</template><script setup>
import { OrganizationSwitcher } from '@clerk/vue'
import { dark } from '@clerk/themes'
</script>
🗃️ 状态管理
1. Pinia Store集成
// src/stores/app/methods.ts
import { defineStore } from 'pinia'
import { onMounted, reactive } from 'vue'
import { useCookie, useRouter } from '#app'
import { useRuntimeConfig } from '#imports'
import { useAuth, useClerk } from '@clerk/vue'
import type { LoginType } from './types'export const useAppStore = defineStore('app', () => {// Clerk hooks 引用let auth: any = nulllet clerk: any = nulllet clerkInitialized = falseconst router = useRouter()// 安全地初始化 Clerk composablesconst initClerkHooks = () => {try {if (import.meta.client &&typeof window !== 'undefined' &&!clerkInitialized) {const config = useRuntimeConfig()if (config.public.clerkPublishableKey) {auth = useAuth()clerk = useClerk()clerkInitialized = trueconsole.log('✅ Clerk hooks initialized in store')return true}}return clerkInitialized} catch (error) {console.warn('Failed to initialize Clerk hooks in store:', error)return false}}const state = reactive({loginType: 'normal',agentModel: {network: false,value: '这是测试agent消息',knowledgeBase: false,},token: useCookie('token').value || '',})onMounted(() => {const localToken = localStorage.getItem('token')const cookieToken = useCookie('token').valuestate.token = localToken || cookieToken || ''state.loginType = (localStorage.getItem('loginType') as LoginType) || 'normal'// 如果是 Clerk 登录类型,尝试初始化 Clerk hooksif (state.loginType === 'clerk') {const initialized = initClerkHooks()if (initialized) {console.log('✅ Clerk hooks initialized on mount')}}})const setToken = (token: string, loginType: LoginType = 'normal') => {state.token = tokenstate.loginType = loginTypelocalStorage.setItem('token', token)localStorage.setItem('loginType', loginType)useCookie('token').value = tokenrouter.push('/')}const logout = async () => {try {// 如果是 Clerk 登录类型,必须确保 Clerk 登出if (state.loginType === 'clerk') {console.log('�� 执行 Clerk 登出...')// 如果还没初始化,尝试初始化if (!clerkInitialized) {console.log('📡 Clerk 未初始化,尝试初始化...')initClerkHooks()}// 确保在客户端环境下执行 Clerk 登出if (import.meta.client && typeof window !== 'undefined') {let clerkLogoutSuccess = false// 尝试使用 auth.signOutif (auth?.signOut?.value) {try {await auth.signOut.value()clerkLogoutSuccess = trueconsole.log('✅ auth.signOut 执行成功')} catch (error) {console.warn('⚠️ auth.signOut 失败:', error)}}// 尝试使用 clerk.signOut(备用方案)if (clerk?.value?.signOut && !clerkLogoutSuccess) {try {await clerk.value.signOut()clerkLogoutSuccess = trueconsole.log('✅ clerk.signOut 执行成功')} catch (error) {console.warn('⚠️ clerk.signOut 失败:', error)}}// 如果 Clerk 登出都失败了,至少清理 Clerk cookiesif (!clerkLogoutSuccess) {console.warn('🚨 Clerk 登出失败,手动清理 Clerk cookies...')const clerkCookies = ['__client','__client_uat','__session','__clerk_db_jwt','__clerk_hs_db_jwt',]clerkCookies.forEach(cookieName => {document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`})}} else {console.warn('⚠️ 不在客户端环境,跳过 Clerk 登出')}}} catch (e) {console.error('❌ Clerk 登出过程出错:', e)}// 清理本地状态(无论 Clerk 登出是否成功)console.log('🧹 清理本地状态...')state.token = ''state.loginType = 'normal'localStorage.removeItem('token')localStorage.removeItem('loginType')const tokenCookie = useCookie('token')tokenCookie.value = nullconsole.log('✅ 登出完成')// 智能跳转:只有在需要认证的页面才跳转if (import.meta.client && typeof window !== 'undefined') {const currentPath = window.location.pathnameconst protectedRoutes = ['/chat', '/dashboard', '/profile']const isProtectedRoute = protectedRoutes.some(route =>currentPath.startsWith(route))if (isProtectedRoute) {console.log(`�� 当前在受保护页面 ${currentPath},跳转到首页`)router.push('/')} else {console.log(`📍 当前在公开页面 ${currentPath},无需跳转`)}}}return {value: state,setToken,logout,}
})
2. 类型定义
// src/stores/app/types.ts
// 登录类型
export type LoginType = 'normal' | 'google' | 'github' | 'clerk'export interface AppState {isLoading: booleanuser: any | nullisAuthenticated: boolean
}
样式定制
1. CSS样式覆盖
/* src/assets/css/main.css *//* Clerk 样式覆盖 */
.cl-card.cl-signIn-start {gap: 0 !important;
}.cl-main {gap: 0 !important;
}.cl-cardBox {box-shadow: none !important;border-radius: unset !important;
}.cl-formButtonPrimary {background-color: #32f08b !important;border: none !important;box-shadow: none !important;color: #1a1b1d !important;border-radius: 6px !important;height: 44px !important;
}.cl-socialButtons {display: flex !important;flex-direction: column !important;gap: 12px !important;width: 100% !important;
}.cl-socialButtonsButton {height: 48px !important;
}.cl-socialButtonsButtonText {font-size: 16px !important;font-weight: 400 !important;
}.cl-dividerRow {margin: 18px 0 !important;
}.cl-dividerText {background-color: transparent !important;
}.cl-dividerLine {background-color: rgba(255, 255, 255, 0.1) !important;
}.cl-formFieldInput {max-height: fit-content !important;height: 48px !important;
}.cl-otpCodeFieldInput {border-color: rgba(255, 255, 255, 0.2) !important;
}
2. 主题配置
// 深色主题配置
const darkTheme = {baseTheme: dark,variables: {colorPrimary: '#00d084',colorBackground: 'transparent',colorInputBackground: '#2a2a2a',colorInputText: '#ffffff',colorText: '#ffffff',colorTextSecondary: '#9ca3af',colorTextOnPrimaryBackground: '#ffffff',colorNeutral: '#404040',borderRadius: '8px',fontSize: '16px',spacingUnit: '1rem',},elements: {card: {backgroundColor: 'transparent',boxShadow: 'none',border: 'none',padding: '0',},headerTitle: {display: 'none',},socialButtonsBlock: {display: 'flex',flexDirection: 'column',gap: '12px',width: '100%',},socialButtonsBlockButton: {height: '48px',backgroundColor: '#2a2a2a',borderColor: '#404040',color: '#ffffff',borderRadius: '8px',border: '1px solid #404040',fontSize: '16px',fontWeight: '400',width: '100%',display: 'flex',alignItems: 'center',justifyContent: 'center',marginBottom: '0','&:hover': {backgroundColor: '#333333',borderColor: '#404040',},'&:focus': {backgroundColor: '#333333',borderColor: '#404040',boxShadow: 'none',},},formButtonPrimary: {backgroundColor: '#00d084',color: '#ffffff',borderRadius: '8px',height: '48px',fontSize: '16px',fontWeight: '500','&:hover': {backgroundColor: '#00b876',},},},
}
🛡️ 路由保护
1. 中间件配置
// src/middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {const { isSignedIn } = useAuth()// 需要认证的路由const protectedRoutes = ['/chat', '/dashboard', '/profile']const isProtectedRoute = protectedRoutes.some(route => to.path.startsWith(route))if (isProtectedRoute && !isSignedIn.value) {return navigateTo('/login')}
})
2. 页面级保护
<!-- src/pages/chat/index.vue -->
<template><div v-if="isSignedIn"><!-- 受保护的内容 --><ChatInterface /></div><div v-else><p>请先登录</p><NuxtLink to="/login">去登录</NuxtLink></div>
</template><script setup>
import { useAuth } from '@clerk/vue'const { isSignedIn } = useAuth()
</script>
3. 组件级保护
<!-- src/components/ProtectedComponent.vue -->
<template><div v-if="isSignedIn"><slot /></div><div v-else><slot name="fallback"><p>需要登录才能查看此内容</p></slot></div>
</template><script setup>
import { useAuth } from '@clerk/vue'const { isSignedIn } = useAuth()
</script>
🔌 API集成
1. 服务端API
// server/api/user.ts
import { clerkClient } from '@clerk/clerk-sdk-node'export default defineEventHandler(async (event) => {try {// 获取当前用户const { userId } = event.context.authif (!userId) {throw createError({statusCode: 401,statusMessage: 'Unauthorized'})}// 获取用户信息const user = await clerkClient.users.getUser(userId)return {id: user.id,email: user.emailAddresses[0]?.emailAddress,firstName: user.firstName,lastName: user.lastName,imageUrl: user.imageUrl,}} catch (error) {console.error('Error fetching user:', error)throw createError({statusCode: 500,statusMessage: 'Internal Server Error'})}
})
2. 客户端API调用
// src/composables/useUser.ts
import { useAuth } from '@clerk/vue'export const useUser = () => {const { isSignedIn, user } = useAuth()const fetchUserData = async () => {if (!isSignedIn.value) {throw new Error('User not authenticated')}try {const response = await $fetch('/api/user')return response} catch (error) {console.error('Error fetching user data:', error)throw error}}return {isSignedIn,user,fetchUserData,}
}
3. 组织API
// server/api/organization.ts
import { clerkClient } from '@clerk/clerk-sdk-node'export default defineEventHandler(async (event) => {try {const { userId } = event.context.authif (!userId) {throw createError({statusCode: 401,statusMessage: 'Unauthorized'})}// 获取用户所属组织const memberships = await clerkClient.users.getOrganizationMembershipList({userId})return memberships.data} catch (error) {console.error('Error fetching organizations:', error)throw createError({statusCode: 500,statusMessage: 'Internal Server Error'})}
})
🚀 最佳实践
1. 错误处理
// src/composables/useClerkError.ts
export const useClerkError = () => {const handleClerkError = (error: any) => {console.error('Clerk Error:', error)switch (error.code) {case 'form_identifier_not_found':return '邮箱地址不存在'case 'form_password_incorrect':return '密码错误'case 'form_code_incorrect':return '验证码错误'case 'oauth_callback_error':return '社交登录失败,请重试'default:return '登录失败,请重试'}}return {handleClerkError,}
}
2. 加载状态管理
// src/composables/useClerkLoading.ts
export const useClerkLoading = () => {const isLoading = ref(false)const loadingMessage = ref('')const setLoading = (loading: boolean, message?: string) => {isLoading.value = loadingloadingMessage.value = message || ''}return {isLoading: readonly(isLoading),loadingMessage: readonly(loadingMessage),setLoading,}
}
3. 用户状态同步
// src/composables/useUserSync.ts
export const useUserSync = () => {const { user, isSignedIn } = useAuth()const appStore = useAppStore()// 监听用户状态变化watch(isSignedIn, (signedIn) => {if (signedIn && user.value) {// 同步用户信息到本地状态appStore.setUser({id: user.value.id,email: user.value.emailAddresses[0]?.emailAddress,firstName: user.value.firstName,lastName: user.value.lastName,imageUrl: user.value.imageUrl,})} else {// 清除本地用户信息appStore.clearUser()}})return {user,isSignedIn,}
}
🔧 故障排除
1. 常见问题
问题1: Clerk未初始化
// 解决方案:检查环境变量
console.log('Clerk Key:', process.env.NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY)
问题2: 样式不生效
/* 解决方案:增加CSS优先级 */
.cl-formButtonPrimary {background-color: #32f08b !important;
}
问题3: 路由重定向循环
// 解决方案:检查中间件配置
export default defineNuxtRouteMiddleware((to) => {const { isSignedIn } = useAuth()// 避免在登录页面检查认证状态if (to.path === '/login' && isSignedIn.value) {return navigateTo('/')}
})
2. 调试技巧
// 启用Clerk调试模式
clerk: {debug: true,// ...其他配置
}
3. 性能优化
// 懒加载Clerk组件
const SignIn = defineAsyncComponent(() => import('@clerk/vue').then(m => m.SignIn))
const SignUp = defineAsyncComponent(() => import('@clerk/vue').then(m => m.SignUp))
参考资源
- Clerk官方文档
- Clerk Vue文档
- Clerk Nuxt文档
- Clerk主题定制
- Clerk API参考
这个文档提供了完整的Clerk集成指南,涵盖了从安装配置到高级功能的各个方面。您可以根据项目需求选择相应的功能进行实现。