Clerk 用户认证系统集成文档

Clerk 用户认证系统集成文档

目录

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

概述

Clerk是一个现代化的用户认证和用户管理平台,提供了完整的身份验证解决方案。本项目已集成Clerk,支持多种登录方式:

  • 邮箱密码登录
  • 社交登录 (Google, GitHub, Facebook等)
  • 手机号验证码登录
  • 邮箱验证码登录
  • 多因素认证(MFA)
  • 组织管理
  • 用户资料管理

📦 安装与配置

1. 依赖包安装

项目已安装以下Clerk相关依赖:

json 复制代码
{
  "dependencies": {
    "@clerk/themes": "^2.4.13",
    "@clerk/nuxt": "^1.8.10", 
    "@clerk/vue": "^1.11.4"
  }
}

2. 安装命令

bash 复制代码
# 使用 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 文件:

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. 基础配置

typescript 复制代码
// 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. 高级配置选项

typescript 复制代码
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. 登录页面集成

vue 复制代码
<!-- src/pages/login/index.tsx -->
<template>
  <div class="login-container">
    <h1>Sign in</h1>
    
    <SignIn
      transferable={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. 注册页面

vue 复制代码
<!-- src/pages/sign-up/index.tsx -->
<template>
  <div class="signup-container">
    <h1>Create Account</h1>
    
    <SignUp
      transferable={true}
      withSignIn={true}
      oauthFlow="redirect"
      appearance={{
        baseTheme: dark,
        // 与登录页面相同的样式配置
      }}
    />
  </div>
</template>

<script setup>
import { SignUp } from '@clerk/vue'
import { dark } from '@clerk/themes'
</script>

3. 用户按钮组件

vue 复制代码
<!-- src/components/UserButton.vue -->
<template>
  <UserButton
    appearance={{
      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. 组织管理组件

vue 复制代码
<!-- src/components/OrganizationSwitcher.vue -->
<template>
  <OrganizationSwitcher
    appearance={{
      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集成

typescript 复制代码
// 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 = null
  let clerk: any = null
  let clerkInitialized = false
  const router = useRouter()

  // 安全地初始化 Clerk composables
  const initClerkHooks = () => {
    try {
      if (
        import.meta.client &&
        typeof window !== 'undefined' &&
        !clerkInitialized
      ) {
        const config = useRuntimeConfig()
        if (config.public.clerkPublishableKey) {
          auth = useAuth()
          clerk = useClerk()
          clerkInitialized = true
          console.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').value

    state.token = localToken || cookieToken || ''
    state.loginType = (localStorage.getItem('loginType') as LoginType) || 'normal'

    // 如果是 Clerk 登录类型,尝试初始化 Clerk hooks
    if (state.loginType === 'clerk') {
      const initialized = initClerkHooks()
      if (initialized) {
        console.log('✅ Clerk hooks initialized on mount')
      }
    }
  })

  const setToken = (token: string, loginType: LoginType = 'normal') => {
    state.token = token
    state.loginType = loginType
    localStorage.setItem('token', token)
    localStorage.setItem('loginType', loginType)
    useCookie('token').value = token
    router.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.signOut
          if (auth?.signOut?.value) {
            try {
              await auth.signOut.value()
              clerkLogoutSuccess = true
              console.log('✅ auth.signOut 执行成功')
            } catch (error) {
              console.warn('⚠️ auth.signOut 失败:', error)
            }
          }

          // 尝试使用 clerk.signOut(备用方案)
          if (clerk?.value?.signOut && !clerkLogoutSuccess) {
            try {
              await clerk.value.signOut()
              clerkLogoutSuccess = true
              console.log('✅ clerk.signOut 执行成功')
            } catch (error) {
              console.warn('⚠️ clerk.signOut 失败:', error)
            }
          }

          // 如果 Clerk 登出都失败了,至少清理 Clerk cookies
          if (!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 = null

    console.log('✅ 登出完成')

    // 智能跳转:只有在需要认证的页面才跳转
    if (import.meta.client && typeof window !== 'undefined') {
      const currentPath = window.location.pathname
      const 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. 类型定义

typescript 复制代码
// src/stores/app/types.ts
// 登录类型
export type LoginType = 'normal' | 'google' | 'github' | 'clerk'

export interface AppState {
  isLoading: boolean
  user: any | null
  isAuthenticated: boolean
}

样式定制

1. CSS样式覆盖

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. 主题配置

typescript 复制代码
// 深色主题配置
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. 中间件配置

typescript 复制代码
// 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. 页面级保护

vue 复制代码
<!-- 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. 组件级保护

vue 复制代码
<!-- 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

typescript 复制代码
// server/api/user.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

export default defineEventHandler(async (event) => {
  try {
    // 获取当前用户
    const { userId } = event.context.auth
    
    if (!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调用

typescript 复制代码
// 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

typescript 复制代码
// server/api/organization.ts
import { clerkClient } from '@clerk/clerk-sdk-node'

export default defineEventHandler(async (event) => {
  try {
    const { userId } = event.context.auth
    
    if (!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. 错误处理

typescript 复制代码
// 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. 加载状态管理

typescript 复制代码
// src/composables/useClerkLoading.ts
export const useClerkLoading = () => {
  const isLoading = ref(false)
  const loadingMessage = ref('')
  
  const setLoading = (loading: boolean, message?: string) => {
    isLoading.value = loading
    loadingMessage.value = message || ''
  }
  
  return {
    isLoading: readonly(isLoading),
    loadingMessage: readonly(loadingMessage),
    setLoading,
  }
}

3. 用户状态同步

typescript 复制代码
// 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未初始化
typescript 复制代码
// 解决方案:检查环境变量
console.log('Clerk Key:', process.env.NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY)
问题2: 样式不生效
css 复制代码
/* 解决方案:增加CSS优先级 */
.cl-formButtonPrimary {
  background-color: #32f08b !important;
}
问题3: 路由重定向循环
typescript 复制代码
// 解决方案:检查中间件配置
export default defineNuxtRouteMiddleware((to) => {
  const { isSignedIn } = useAuth()
  
  // 避免在登录页面检查认证状态
  if (to.path === '/login' && isSignedIn.value) {
    return navigateTo('/')
  }
})

2. 调试技巧

typescript 复制代码
// 启用Clerk调试模式
clerk: {
  debug: true,
  // ...其他配置
}

3. 性能优化

typescript 复制代码
// 懒加载Clerk组件
const SignIn = defineAsyncComponent(() => import('@clerk/vue').then(m => m.SignIn))
const SignUp = defineAsyncComponent(() => import('@clerk/vue').then(m => m.SignUp))

参考资源


这个文档提供了完整的Clerk集成指南,涵盖了从安装配置到高级功能的各个方面。您可以根据项目需求选择相应的功能进行实现。

相关推荐
CodeTransfer1 小时前
css中animation与js的绑定原来还能这样玩。。。
前端·javascript
言之。2 小时前
Web技术构建桌面应用-Tauri框架和Electron框架
前端·javascript·electron
萌萌哒草头将军2 小时前
Node.js v24.7.0 新功能预览 🚀🚀🚀
前端·javascript·node.js
程序员张32 小时前
Vue3+ElementPlus—高效存储和回显多选项的状态值
javascript·vue.js·前端框架
艾小码2 小时前
90%前端忽略的3大内存黑洞,这样根治性能飙升300%!
前端·javascript·性能优化
GISer_Jing2 小时前
React Native核心技术深度解析_Trip Footprints
javascript·react native·react.js
Mintopia3 小时前
AIGC 多模态大模型在 Web 场景中的融合技术与挑战
前端·javascript·aigc
Mintopia3 小时前
🛡️ Next.js 中间件权限验证与 API 保护的奇幻冒险
前端·javascript·next.js
Miracle_G3 小时前
每日一个知识点:几分钟学会页面拖拽分隔布局的实现
前端·javascript