当前位置: 首页 > backend >正文

Clerk 用户认证系统集成文档

Clerk 用户认证系统集成文档

目录

  1. 概述
  2. 安装与配置
  3. 环境变量配置
  4. Nuxt配置
  5. 组件集成
  6. 状态管理
  7. 样式定制
  8. 路由保护
  9. API集成
  10. 最佳实践
  11. 故障排除

概述

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密钥

  1. 访问 Clerk Dashboard
  2. 创建新应用或选择现有应用
  3. 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集成指南,涵盖了从安装配置到高级功能的各个方面。您可以根据项目需求选择相应的功能进行实现。

http://www.xdnf.cn/news/18833.html

相关文章:

  • ollama离线部署+大语言模型
  • AI-调查研究-62-机器人 机械臂五大应用场景详解:从焊接到手术,从农田到太空
  • 4步用代码拆解数学建模中的TOPSIS评价决策! ! !
  • Apache Commons Lang 3
  • 野火STM32Modbus主机读取寄存器/线圈失败(二)-解决CRC校验错误
  • uC/OS-III 队列相关接口
  • 数据分析与数据挖掘
  • 企业如何构建全面的高防IP防护体系?
  • Teams Workflows 业务流程搭建与Linux自动化运维拓展应用全解析
  • 状态设计模式
  • 构建面向人工智能决策的世界模型引擎所需的基本知识体系
  • 如何在GitHub找到10k+个stars的仓库
  • podman启动mongdb的container因为权限问题导致changing ownership和读取storage.bson失败的解决方法
  • CMake构建学习笔记20-iconv库的构建
  • 算法概述篇
  • 游戏空间划分技术
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段(20):文法+单词第7回2
  • 广告推荐模型1:逻辑回归(Logistic Regression,LR)
  • 如何拯救一家濒临破产的科技公司?
  • 技术总结:AArch64架构下Jenkins Agent(RPM容器编译节点)掉线问题分析与排查
  • KubeBlocks for Oracle 容器化之路
  • 【RAGFlow代码详解-30】构建系统和 CI/CD
  • 微服务-28.配置管理-共享配置
  • poi生成word固定表格列宽
  • TensorFlow 面试题及详细答案 120道(61-70)-- 高级特性与工具
  • css3背景线性渐变:linear-gradient
  • 【密集目标检测】停车场车辆(车位)识别数据集:12k+图像,yolo标注
  • 04 网络信息内容安全--入侵检测技术
  • 依托边缘计算方案,移动云全面化解算力、效率、安全平衡难题
  • from中烟科技翼支付 面试题2