Acme

认证配置

了解如何为私有组件注册表添加身份验证,保护您的组件资源。

本指南将引导您完成为私有组件注册表添加身份验证(Authentication)的过程。通过添加认证,您可以确保只有授权用户才能访问您的注册表组件。

概述

注册表认证是可选的。您可以选择:

  • 公开注册表:不设置 REGISTRY_TOKEN 环境变量,任何人都可以自由访问您的组件
  • 私有注册表:设置 REGISTRY_TOKEN 环境变量,只有授权用户才能访问

私有注册表认证允许您:

  • 保护敏感组件:限制对私有或专有组件的访问
  • 控制访问权限:管理谁可以安装和使用您的组件
  • 跟踪使用情况:通过令牌识别和监控组件的使用
  • 实现企业安全:满足组织的安全和合规要求

如果您的组件是开源的或希望公开分享,可以跳过认证配置。如果您需要保护敏感组件或控制访问权限,请继续阅读本指南。

认证方法

本注册表支持三种认证方法,您可以根据需要选择使用:

Bearer Token(推荐)

通过 Authorization 请求头传递 Bearer Token:

Authorization: Bearer YOUR_TOKEN_HERE

API Key

通过自定义 X-API-Key 请求头传递 API Key:

X-API-Key: YOUR_API_KEY_HERE

查询参数

通过 URL 查询参数传递令牌(不推荐用于生产环境):

?token=YOUR_TOKEN_HERE

安全提示: 查询参数方式会在 URL 中暴露令牌,可能被记录在服务器日志、浏览器历史或代理服务器中。仅建议在开发环境或临时测试时使用。生产环境请使用 Bearer Token 或 API Key 方式。

服务端配置

设置环境变量

认证是可选的。如果您希望将注册表设置为私有,需要配置 REGISTRY_TOKEN 环境变量。如果不配置此变量,注册表将对所有人公开访问。

在项目根目录创建 .env.local 文件(或添加到现有文件):

.env.local
# Registry authentication token (可选)
# 如果不设置,注册表将是公开的
REGISTRY_TOKEN=your_secret_token_here

注意:

  • 如果设置了 REGISTRY_TOKEN,则需要提供有效的认证令牌才能访问注册表
  • 如果未设置 REGISTRY_TOKEN,注册表将对所有人公开,无需认证
  • 确保将 .env.local 添加到 .gitignore 文件中,避免将敏感信息提交到版本控制系统

生成安全令牌

建议生成一个强随机令牌作为认证密钥。您可以使用以下方法之一:

使用 Node.js

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

使用 OpenSSL

openssl rand -hex 32

使用 UUID

uuidgen

保护静态文件

Registry 的 JSON 文件存放在 public/r/ 目录下。默认情况下,Next.js 会将 public 目录下的文件作为静态资源直接提供。

如果您配置了 REGISTRY_TOKEN,需要使用 Next.js Middleware 来保护这些文件:

middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  // 只拦截 /r/ 路径下的请求(registry 静态文件)
  if (request.nextUrl.pathname.startsWith("/r/")) {
    // 检查是否配置了认证令牌
    const validToken = process.env.REGISTRY_TOKEN;

    // 如果未配置 REGISTRY_TOKEN,则不需要认证,直接放行
    if (!validToken) {
      return NextResponse.next();
    }

    // 如果配置了 REGISTRY_TOKEN,则需要进行认证
    // 获取认证令牌
    const authHeader = request.headers.get("authorization");
    const bearerToken = authHeader?.replace("Bearer ", "");
    const apiKey = request.headers.get("x-api-key");
    const queryToken = request.nextUrl.searchParams.get("token");

    const token = bearerToken || apiKey || queryToken;

    // 检查令牌是否提供
    if (!token) {
      return NextResponse.json(
        {
          error: "Unauthorized",
          message: "认证令牌缺失"
        },
        { status: 401 }
      );
    }

    // 验证令牌
    if (token !== validToken) {
      return NextResponse.json(
        {
          error: "Unauthorized",
          message: "令牌无效"
        },
        { status: 401 }
      );
    }

    // 令牌有效,允许访问
    return NextResponse.next();
  }

  // 其他路径不需要认证
  return NextResponse.next();
}

export const config = {
  matcher: ["/r/:path*"], // 匹配所有 /r/ 开头的路径
};

工作原理

  • 如果未设置 REGISTRY_TOKEN 环境变量,Middleware 会直接放行所有请求,注册表是公开的
  • 如果设置了 REGISTRY_TOKEN,Middleware 会验证请求中的认证令牌
  • 这让您可以灵活地在开发环境中公开注册表,在生产环境中启用保护

安全提醒:如果您设置了 REGISTRY_TOKEN 但不使用 Middleware 保护 /r/ 路径,任何人都可以直接访问 https://your-domain.com/r/component.json 而无需认证!请确保在配置了认证令牌后,同时使用 Middleware 进行保护。

API 路由实现

认证逻辑在 app/api/registry/[name]/route.ts 中实现:

app/api/registry/[name]/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(
  request: NextRequest,
  { params }: { params: { name: string } }
) {
  // 从 Authorization 请求头获取 Bearer token
  const authHeader = request.headers.get("authorization");
  const bearerToken = authHeader?.replace("Bearer ", "");

  // 或从 X-API-Key 请求头获取
  const apiKey = request.headers.get("x-api-key");

  // 或从查询参数获取
  const queryToken = request.nextUrl.searchParams.get("token");

  const token = bearerToken || apiKey || queryToken;

  // 验证令牌
  if (!token) {
    return NextResponse.json(
      { error: "Unauthorized", message: "未提供认证令牌" },
      { status: 401 }
    );
  }

  if (!isValidToken(token)) {
    return NextResponse.json(
      { error: "Unauthorized", message: "无效的认证令牌" },
      { status: 401 }
    );
  }

  // 检查访问权限
  if (!hasAccessToComponent(token, params.name)) {
    return NextResponse.json(
      { error: "Forbidden", message: "您没有权限访问此组件" },
      { status: 403 }
    );
  }

  // 返回组件内容
  const component = await getComponent(params.name);
  return NextResponse.json(component);
}

function isValidToken(token: string): boolean {
  return token === process.env.REGISTRY_TOKEN;
}

客户端配置

公开注册表

如果注册表未配置 REGISTRY_TOKEN(公开注册表),您可以直接使用注册表 URL,无需配置任何认证信息:

components.json
{
  "registries": {
    "@acme": "https://registry.acme.com/r/{name}.json"
  }
}

然后直接安装组件:

npx shadcn@latest add @acme/button

私有注册表配置

要从需要认证的私有注册表安装组件,需要在 components.json 中配置认证信息:

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

设置客户端环境变量

在客户端项目中创建 .env.local 文件:

.env.local
REGISTRY_TOKEN=your_secret_token_here

注意: 环境变量中的 ${REGISTRY_TOKEN} 会自动从 process.env.REGISTRY_TOKEN 中读取。确保在安装组件之前设置了此环境变量。

安装组件

配置完成后,使用 shadcn 命令行工具安装组件:

npx shadcn@latest add @acme/button

CLI 会自动:

  1. components.json 读取注册表配置
  2. 从环境变量中获取 REGISTRY_TOKEN
  3. 将令牌添加到 Authorization 请求头
  4. 向注册表 API 发送认证请求
  5. 下载并安装组件

高级配置

多种认证方式

您可以同时配置多个认证请求头:

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

对应的环境变量:

.env.local
ACCESS_TOKEN=your_access_token
API_KEY=your_api_key
WORKSPACE_ID=your_workspace_id

基于角色的访问控制(RBAC)

您可以扩展认证逻辑以实现更细粒度的权限控制:

app/api/registry/[name]/route.ts
function hasAccessToComponent(token: string, componentName: string): boolean {
  // 从数据库或 JWT 中获取用户角色
  const userRole = getUserRoleFromToken(token);

  // 定义组件访问规则
  const componentAccess = {
    'premium-button': ['admin', 'premium'],
    'basic-button': ['admin', 'premium', 'basic'],
    'internal-utils': ['admin']
  };

  const allowedRoles = componentAccess[componentName] || [];
  return allowedRoles.includes(userRole);
}

JWT Token 验证

对于更安全的认证,可以使用 JWT(JSON Web Token):

app/api/registry/[name]/route.ts
import { jwtVerify } from 'jose';

async function isValidToken(token: string): Promise<boolean> {
  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // 检查令牌是否过期
    if (payload.exp && payload.exp < Date.now() / 1000) {
      return false;
    }

    return true;
  } catch (error) {
    return false;
  }
}

安装 JWT 库:

npm install jose

数据库验证

对于企业级应用,建议将令牌存储在数据库中:

app/api/registry/[name]/route.ts
import { prisma } from '@/lib/prisma';

async function isValidToken(token: string): Promise<boolean> {
  const apiKey = await prisma.apiKey.findUnique({
    where: { token },
    include: { user: true }
  });

  if (!apiKey || !apiKey.isActive) {
    return false;
  }

  // 检查令牌是否过期
  if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
    return false;
  }

  // 更新最后使用时间
  await prisma.apiKey.update({
    where: { id: apiKey.id },
    data: { lastUsedAt: new Date() }
  });

  return true;
}

安全最佳实践

使用 HTTPS

始终通过 HTTPS 提供注册表服务,确保令牌在传输过程中加密:

components.json
{
  "registries": {
    "@secure": "https://registry.example.com/{name}.json" // ✅ 推荐
    // "@insecure": "http://registry.example.com/{name}.json" // ❌ 避免
  }
}

令牌轮换

定期更新注册表令牌,降低令牌泄露的风险:

  • 设置令牌过期时间
  • 实现令牌刷新机制
  • 支持令牌撤销功能

环境变量管理

  • ✅ 使用 .env.local 存储敏感信息
  • ✅ 将 .env.local 添加到 .gitignore
  • ✅ 在 CI/CD 中使用加密的环境变量
  • ❌ 不要将令牌硬编码在代码中
  • ❌ 不要将 .env.local 提交到版本控制

速率限制

实现 API 速率限制,防止滥用:

app/api/registry/[name]/route.ts
import { ratelimit } from '@/lib/ratelimit';

export async function GET(request: NextRequest) {
  const ip = request.ip || 'anonymous';
  const { success, limit, reset, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: "Too Many Requests" },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        }
      }
    );
  }

  // 继续处理请求...
}

日志和监控

记录认证失败和异常访问行为:

app/api/registry/[name]/route.ts
function logAuthFailure(token: string, componentName: string, ip: string) {
  console.warn('Authentication failed', {
    component: componentName,
    ip,
    timestamp: new Date().toISOString(),
    // 不要记录完整令牌
    tokenPrefix: token.substring(0, 8) + '...'
  });
}

registry.json 配置

在注册表的 registry.json 文件中声明需要的认证头:

registry.json
{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "acme",
  "homepage": "https://acme.com",
  "headers": {
    "Authorization": "Bearer ${REGISTRY_TOKEN}"
  },
  "items": [
    {
      "name": "button",
      "type": "registry:ui",
      "description": "A customizable button component"
    }
  ]
}

测试认证

测试公开注册表

如果未配置 REGISTRY_TOKEN,注册表应该可以直接访问:

# 应该成功返回组件内容
curl https://registry.acme.com/r/button.json

测试私有注册表

如果配置了 REGISTRY_TOKEN,使用 curl 测试认证:

# 测试 Bearer Token
curl -H "Authorization: Bearer your_token_here" \
  https://registry.acme.com/r/button.json

# 测试 API Key
curl -H "X-API-Key: your_api_key_here" \
  https://registry.acme.com/r/button.json

# 测试查询参数
curl "https://registry.acme.com/r/button.json?token=your_token_here"

测试错误响应

# 未提供令牌 - 应返回 401(仅当配置了 REGISTRY_TOKEN)
curl https://registry.acme.com/r/button.json

# 无效令牌 - 应返回 401(仅当配置了 REGISTRY_TOKEN)
curl -H "Authorization: Bearer invalid_token" \
  https://registry.acme.com/r/button.json

常见问题

如何判断注册表是公开还是私有?

您可以尝试不带认证信息访问注册表:

curl https://registry.acme.com/r/registry.json
  • 如果成功返回内容,说明是公开注册表
  • 如果返回 401 Unauthorized,说明是私有注册表,需要提供认证令牌

认证失败

如果遇到 401 Unauthorized 错误:

  1. 确认是否需要认证:检查注册表是否配置了 REGISTRY_TOKEN
  2. 检查环境变量:确认 REGISTRY_TOKEN 已正确设置
  3. 验证令牌格式:确保 Bearer Token 格式正确
  4. 检查令牌有效性:确认令牌未过期且与服务器匹配
  5. 查看服务器日志:检查服务器端的错误信息
# 检查环境变量
echo $REGISTRY_TOKEN

# 重新加载环境变量
source .env.local

403 Forbidden 错误

如果遇到 403 Forbidden 错误:

  • 令牌有效,但没有访问特定组件的权限
  • 检查 RBAC 配置
  • 联系注册表管理员获取相应权限

环境变量未生效

shadcn CLI 读取环境变量的顺序:

  1. process.env
  2. .env.local
  3. .env

确保在正确的文件中设置了环境变量。

部署注意事项

Vercel

在 Vercel 中设置环境变量:

  1. 进入项目设置
  2. 导航到 "Environment Variables"
  3. 添加 REGISTRY_TOKEN
  4. 选择环境(Production、Preview、Development)

其他平台

  • Netlify:在 Site settings > Environment variables 中配置
  • AWS:使用 AWS Secrets Manager 或 Parameter Store
  • Docker:通过 -e 参数或 .env 文件传递环境变量
docker run -e REGISTRY_TOKEN=your_token app

相关资源