认证配置
了解如何为私有组件注册表添加身份验证,保护您的组件资源。
本指南将引导您完成为私有组件注册表添加身份验证(Authentication)的过程。通过添加认证,您可以确保只有授权用户才能访问您的注册表组件。
概述
注册表认证是可选的。您可以选择:
- 公开注册表:不设置
REGISTRY_TOKEN环境变量,任何人都可以自由访问您的组件 - 私有注册表:设置
REGISTRY_TOKEN环境变量,只有授权用户才能访问
私有注册表认证允许您:
- 保护敏感组件:限制对私有或专有组件的访问
- 控制访问权限:管理谁可以安装和使用您的组件
- 跟踪使用情况:通过令牌识别和监控组件的使用
- 实现企业安全:满足组织的安全和合规要求
如果您的组件是开源的或希望公开分享,可以跳过认证配置。如果您需要保护敏感组件或控制访问权限,请继续阅读本指南。
认证方法
本注册表支持三种认证方法,您可以根据需要选择使用:
查询参数
通过 URL 查询参数传递令牌(不推荐用于生产环境):
?token=YOUR_TOKEN_HERE安全提示: 查询参数方式会在 URL 中暴露令牌,可能被记录在服务器日志、浏览器历史或代理服务器中。仅建议在开发环境或临时测试时使用。生产环境请使用 Bearer Token 或 API Key 方式。
服务端配置
设置环境变量
认证是可选的。如果您希望将注册表设置为私有,需要配置 REGISTRY_TOKEN 环境变量。如果不配置此变量,注册表将对所有人公开访问。
在项目根目录创建 .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 来保护这些文件:
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 中实现:
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,无需配置任何认证信息:
{
"registries": {
"@acme": "https://registry.acme.com/r/{name}.json"
}
}然后直接安装组件:
npx shadcn@latest add @acme/button私有注册表配置
要从需要认证的私有注册表安装组件,需要在 components.json 中配置认证信息:
{
"registries": {
"@acme": {
"url": "https://registry.acme.com/api/registry/{name}",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}设置客户端环境变量
在客户端项目中创建 .env.local 文件:
REGISTRY_TOKEN=your_secret_token_here注意: 环境变量中的 ${REGISTRY_TOKEN} 会自动从 process.env.REGISTRY_TOKEN 中读取。确保在安装组件之前设置了此环境变量。
安装组件
配置完成后,使用 shadcn 命令行工具安装组件:
npx shadcn@latest add @acme/buttonCLI 会自动:
- 从
components.json读取注册表配置 - 从环境变量中获取
REGISTRY_TOKEN - 将令牌添加到
Authorization请求头 - 向注册表 API 发送认证请求
- 下载并安装组件
高级配置
多种认证方式
您可以同时配置多个认证请求头:
{
"registries": {
"@enterprise": {
"url": "https://api.company.com/registry/{name}",
"headers": {
"Authorization": "Bearer ${ACCESS_TOKEN}",
"X-API-Key": "${API_KEY}",
"X-Workspace-Id": "${WORKSPACE_ID}"
}
}
}
}对应的环境变量:
ACCESS_TOKEN=your_access_token
API_KEY=your_api_key
WORKSPACE_ID=your_workspace_id基于角色的访问控制(RBAC)
您可以扩展认证逻辑以实现更细粒度的权限控制:
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):
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数据库验证
对于企业级应用,建议将令牌存储在数据库中:
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 提供注册表服务,确保令牌在传输过程中加密:
{
"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 速率限制,防止滥用:
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(),
}
}
);
}
// 继续处理请求...
}日志和监控
记录认证失败和异常访问行为:
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 文件中声明需要的认证头:
{
"$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 错误:
- 确认是否需要认证:检查注册表是否配置了
REGISTRY_TOKEN - 检查环境变量:确认
REGISTRY_TOKEN已正确设置 - 验证令牌格式:确保 Bearer Token 格式正确
- 检查令牌有效性:确认令牌未过期且与服务器匹配
- 查看服务器日志:检查服务器端的错误信息
# 检查环境变量
echo $REGISTRY_TOKEN
# 重新加载环境变量
source .env.local403 Forbidden 错误
如果遇到 403 Forbidden 错误:
- 令牌有效,但没有访问特定组件的权限
- 检查 RBAC 配置
- 联系注册表管理员获取相应权限
环境变量未生效
shadcn CLI 读取环境变量的顺序:
process.env.env.local.env
确保在正确的文件中设置了环境变量。
部署注意事项
Vercel
在 Vercel 中设置环境变量:
- 进入项目设置
- 导航到 "Environment Variables"
- 添加
REGISTRY_TOKEN - 选择环境(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