98.0k

认证

PreviousNext

通过认证保护您的注册表,实现私有和个性化组件的安全访问。

认证让您可以运行私有注册表,控制谁可以访问您的组件,并为不同的团队或用户提供不同的内容。本指南展示了常见的认证模式及其设置方法。

认证支持以下用例:

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

常见认证模式

基于 Token 的认证

最常用的方法是使用 Bearer token 或 API 密钥:

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

在环境变量中设置您的 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}"
      }
    }
  }
}

这将形成如下 URL: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 } }
) {
  // 从 Authorization 请求头中获取 token。
  const authHeader = request.headers.get("authorization")
  const token = authHeader?.replace("Bearer ", "")
 
  // 或从查询参数中获取。
  const queryToken = request.nextUrl.searchParams.get("token")
 
  // 验证 token 是否有效。
  if (!isValidToken(token || queryToken)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }
 
  // 检查 token 是否有权访问该组件。
  if (!hasAccessToComponent(token, params.name)) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 })
  }
 
  // 返回组件数据。
  const component = await getComponent(params.name)
  return NextResponse.json(component)
}
 
function isValidToken(token: string | null) {
  // 添加您的 token 验证逻辑。
  // 比如数据库查询、JWT 验证等。
  return token === process.env.VALID_TOKEN
}
 
function hasAccessToComponent(token: string, componentName: string) {
  // 添加基于角色的访问控制逻辑。
  // 验证 token 是否能访问指定组件。
  return true // 这里替换为您的逻辑。
}

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)
 
  // 获取该团队可访问的组件。
  const components = await getComponentsForTeam(team)
  return NextResponse.json(components)
}

用户个性化注册表

根据用户偏好为其提供组件:

async function GET(request: NextRequest) {
  const user = await authenticateUser(request)
 
  // 获取用户的风格及框架偏好。
  const preferences = await getUserPreferences(user.id)
 
  // 获取个性化组件版本。
  const component = await getPersonalizedComponent(params.name, preferences)
 
  return NextResponse.json(component)
}

临时访问 Token

使用带过期时间的 token 以增强安全性:

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}"
      }
    }
  }
}

这样可以实现:

  • 混合使用公共和私有注册表
  • 针对不同注册表使用不同的认证方式
  • 按访问权限组织组件

安全最佳实践

使用环境变量

切勿将 tokens 提交至版本控制。始终使用环境变量:

.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 以保护传输中的 token:

{
  "@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 分钟
  max: 100, // 每个 IP 在窗口期内最多 100 次请求
})
 
app.use("/registry", limiter)

轮换 Token

定期更换访问 token:

// 生成带过期时间的新 token。
function generateToken() {
  const token = crypto.randomBytes(32).toString("hex")
  const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 天。
 
  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"],
  })
}

测试认证

本地测试您的认证注册表:

# 使用 curl 测试。
curl -H "Authorization: Bearer your_token" \
  https://registry.company.com/button.json
 
# 使用 CLI 测试。
REGISTRY_TOKEN=your_token npx shadcn@latest add @private/button

错误处理

shadcn CLI 会优雅地处理认证错误:

  • 401 Unauthorized:Token 无效或缺失
  • 403 Forbidden:Token 无访问权限
  • 429 Too Many Requests:请求频率超限

自定义错误消息

您的注册表服务器可以在响应体中返回自定义错误消息,CLI 会展示给用户:

// 注册表服务器返回自定义错误
return NextResponse.json(
  {
    error: "Unauthorized",
    message:
      "您的订阅已过期。请访问 company.com/billing 续订",
  },
  { status: 403 }
)

用户将看到:

您的订阅已过期。请访问 company.com/billing 续订

这有助于提供针对性提示:

// 针对不同场景的不同错误消息
if (!token) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message:
        "需要认证。请在您的 .env.local 文件中设置 REGISTRY_TOKEN",
    },
    { status: 401 }
  )
}
 
if (isExpiredToken(token)) {
  return NextResponse.json(
    {
      error: "Unauthorized",
      message: "Token 已过期。请访问 company.com/tokens 申请新 token",
    },
    { status: 401 }
  )
}
 
if (!hasTeamAccess(token, component)) {
  return NextResponse.json(
    {
      error: "Forbidden",
      message: `组件 '${component}' 仅限设计团队访问`,
    },
    { status: 403 }
  )
}

后续步骤

要设置多注册表及高级认证模式,请参阅 命名空间注册表 文档。内容包括:

  • 多个认证注册表的设置
  • 不同命名空间的认证方式
  • 跨注册表依赖解析
  • 高级认证模式