94.2k

身份验证

上一个下一个

通过身份验证保护您的注册表,实现私有化和个性化组件。

身份验证允许您运行私有注册表,控制谁可以访问您的组件,并为不同的团队或用户提供不同的内容。本指南将展示常见的身份验证模式以及如何设置它们。

身份验证支持以下用例

  • 私有组件:确保您的业务逻辑和内部组件安全
  • 团队专用资源:为不同的团队提供不同的组件
  • 访问控制:限制谁可以查看敏感或实验性组件
  • 使用分析:查看组织中哪些用户正在使用哪些组件
  • 许可:控制谁可以获得高级或授权组件

常见身份验证模式

基于令牌的身份验证

最常见的方法是使用 Bearer 令牌或 API 密钥

components.json
{
  "registries": {
    "@private": {
      "url": "https://registry.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${REGISTRY_TOKEN}"
      }
    }
  }
}

在环境变量中设置您的令牌

.env.local
REGISTRY_TOKEN=your_secret_token_here

API 密钥身份验证

有些注册表在请求头中使用 API 密钥

components.json
{
  "registries": {
    "@company": {
      "url": "https://api.company.com/registry/{name}.json",
      "headers": {
        "X-API-Key": "${API_KEY}",
        "X-Workspace-Id": "${WORKSPACE_ID}"
      }
    }
  }
}

查询参数身份验证

对于更简单的设置,请使用查询参数

components.json
{
  "registries": {
    "@internal": {
      "url": "https://registry.company.com/{name}.json",
      "params": {
        "token": "${ACCESS_TOKEN}"
      }
    }
  }
}

这将创建:https://registry.company.com/button.json?token=your_token

服务器端实现

以下是如何将身份验证添加到您的注册表服务器

Next.js API 路由示例

app/api/registry/[name]/route.ts
import { NextRequest, NextResponse } from "next/server"
 
export async function GET(
  request: NextRequest,
  { params }: { params: { name: string } }
) {
  // Get token from Authorization header.
  const authHeader = request.headers.get("authorization")
  const token = authHeader?.replace("Bearer ", "")
 
  // Or from query parameters.
  const queryToken = request.nextUrl.searchParams.get("token")
 
  // Check if token is valid.
  if (!isValidToken(token || queryToken)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }
 
  // Check if token can access this component.
  if (!hasAccessToComponent(token, params.name)) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 })
  }
 
  // Return the component.
  const component = await getComponent(params.name)
  return NextResponse.json(component)
}
 
function isValidToken(token: string | null) {
  // Add your token validation logic here.
  // Check against database, JWT validation, etc.
  return token === process.env.VALID_TOKEN
}
 
function hasAccessToComponent(token: string, componentName: string) {
  // Add role-based access control here.
  // Check if token can access specific component.
  return true // Your logic here.
}

Express.js 示例

server.js
app.get("/registry/:name.json", (req, res) => {
  const token = req.headers.authorization?.replace("Bearer ", "")
 
  if (!isValidToken(token)) {
    return res.status(401).json({ error: "Unauthorized" })
  }
 
  const component = getComponent(req.params.name)
  if (!component) {
    return res.status(404).json({ error: "Component not found" })
  }
 
  res.json(component)
})

高级身份验证模式

基于团队的访问

为不同的团队提供不同的组件

api/registry/route.ts
async function GET(request: NextRequest) {
  const token = extractToken(request)
  const team = await getTeamFromToken(token)
 
  // Get components for this team.
  const components = await getComponentsForTeam(team)
  return NextResponse.json(components)
}

用户个性化注册表

根据用户的偏好提供组件

async function GET(request: NextRequest) {
  const user = await authenticateUser(request)
 
  // Get user's style and framework preferences.
  const preferences = await getUserPreferences(user.id)
 
  // Get personalized component version.
  const component = await getPersonalizedComponent(params.name, preferences)
 
  return NextResponse.json(component)
}

临时访问令牌

使用过期令牌以提高安全性

interface TemporaryToken {
  token: string
  expiresAt: Date
  scope: string[]
}
 
async function validateTemporaryToken(token: string) {
  const tokenData = await getTokenData(token)
 
  if (!tokenData) return false
  if (new Date() > tokenData.expiresAt) return false
 
  return true
}

多注册表身份验证

通过命名空间注册表,您可以设置多个具有不同身份验证的注册表

components.json
{
  "registries": {
    "@public": "https://public.company.com/{name}.json",
    "@internal": {
      "url": "https://internal.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${INTERNAL_TOKEN}"
      }
    },
    "@premium": {
      "url": "https://premium.company.com/{name}.json",
      "headers": {
        "X-License-Key": "${LICENSE_KEY}"
      }
    }
  }
}

这使您可以

  • 混合公共和私有注册表
  • 每个注册表使用不同的身份验证
  • 按访问级别组织组件

安全最佳实践

使用环境变量

切勿将令牌提交到版本控制。始终使用环境变量

.env.local
REGISTRY_TOKEN=your_secret_token_here
API_KEY=your_api_key_here

然后在components.json中引用它们

{
  "registries": {
    "@private": {
      "url": "https://registry.company.com/{name}.json",
      "headers": {
        "Authorization": "Bearer ${REGISTRY_TOKEN}"
      }
    }
  }
}

使用 HTTPS

始终使用 HTTPS URL 用于注册表,以保护传输中的令牌

{
  "@secure": "https://registry.company.com/{name}.json" // ✅
  "@insecure": "http://registry.company.com/{name}.json" // ❌
}

添加速率限制

保护您的注册表免受滥用

import rateLimit from "express-rate-limit"
 
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
})
 
app.use("/registry", limiter)

轮换令牌

定期更改访问令牌

// Create new token with expiration.
function generateToken() {
  const token = crypto.randomBytes(32).toString("hex")
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days.
 
  return { token, expiresAt }
}

记录访问

跟踪注册表访问以实现安全和分析

async function logAccess(request: Request, component: string, userId: string) {
  await db.accessLog.create({
    timestamp: new Date(),
    userId,
    component,
    ip: request.ip,
    userAgent: request.headers["user-agent"],
  })
}

测试身份验证

在本地测试您的已验证注册表

# Test with curl.
curl -H "Authorization: Bearer your_token" \
  https://registry.company.com/button.json
 
# Test with the CLI.
REGISTRY_TOKEN=your_token npx shadcn@latest add @private/button

错误处理

shadcn CLI 优雅地处理身份验证错误

  • 401 未授权:令牌无效或缺失
  • 403 禁止:令牌没有访问此资源的权限
  • 429 请求过多:超出速率限制

自定义错误消息

您的注册表服务器可以在响应正文中返回自定义错误消息,CLI 将向用户显示这些消息

// Registry server returns custom error
return NextResponse.json(
  {
    error: "Unauthorized",
    message:
      "Your subscription has expired. Please renew at company.com/billing",
  },
  { status: 403 }
)

用户将看到

Your subscription has expired. Please renew at company.com/billing

这有助于提供上下文相关的指导

// Different error messages for different scenarios
if (!token) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message:
        "Authentication required. Set REGISTRY_TOKEN in your .env.local file",
    },
    { status: 401 }
  )
}
 
if (isExpiredToken(token)) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message: "Token expired. Request a new token at company.com/tokens",
    },
    { status: 401 }
  )
}
 
if (!hasTeamAccess(token, component)) {
  return NextResponse.json(
    {
      error: "Forbidden",
      message: `Component '${component}' is restricted to the Design team`,
    },
    { status: 403 }
  )
}

后续步骤

要设置多注册表身份验证和高级模式,请参阅命名空间注册表文档。它涵盖了

  • 设置多个已验证的注册表
  • 每个命名空间使用不同的身份验证
  • 跨注册表依赖解析
  • 高级身份验证模式