组件
- 手风琴 (Accordion)
- 警示框 (Alert)
- 对话框 (Alert Dialog)
- 宽高比 (Aspect Ratio)
- 头像 (Avatar)
- 徽章 (Badge)
- 面包屑 (Breadcrumb)
- 按钮 (Button)
- 按钮组 (Button Group)
- 日历 (Calendar)
- 卡片 (Card)
- 轮播 (Carousel)
- 图表 (Chart)
- 复选框 (Checkbox)
- 折叠面板 (Collapsible)
- 组合框 (Combobox)
- 命令栏 (Command)
- 上下文菜单 (Context Menu)
- 数据表格 (Data Table)
- 日期选择器 (Date Picker)
- 对话框 (Dialog)
- 方向 (Direction)
- 抽屉 (Drawer)
- 下拉菜单 (Dropdown Menu)
- 空状态 (Empty)
- 字段 (Field)
- 悬浮卡片 (Hover Card)
- 输入框 (Input)
- 输入框组 (Input Group)
- OTP 输入框 (Input OTP)
- 项目 (Item)
- 快捷键 (Kbd)
- 标签 (Label)
- 菜单栏 (Menubar)
- 原生选择框 (Native Select)
- 导航菜单 (Navigation Menu)
- 分页 (Pagination)
- 气泡卡片 (Popover)
- 进度条 (Progress)
- 单选组 (Radio Group)
- 可调节大小 (Resizable)
- 滚动区域 (Scroll Area)
- 选择框 (Select)
- 分隔线 (Separator)
- 侧边栏 (Sheet)
- 导航侧边栏 (Sidebar)
- 骨架屏 (Skeleton)
- 滑块 (Slider)
- Sonner (吐司通知)
- 加载器 (Spinner)
- 开关 (Switch)
- 表格 (Table)
- 标签页 (Tabs)
- 文本域 (Textarea)
- 吐司 (Toast)
- 切换按钮 (Toggle)
- 切换组 (Toggle Group)
- 文字提示 (Tooltip)
- 排版 (Typography)
身份验证允许您运行私有注册表,控制谁可以访问您的组件,并为不同的团队或用户提供不同的内容。本指南介绍了常见的身份验证模式以及如何进行设置。
身份验证可实现以下用例
- 私有组件:确保您的业务逻辑和内部组件安全
- 团队专属资源:为不同团队提供不同的组件
- 访问控制:限制谁可以查看敏感或实验性组件
- 使用分析:查看组织中是谁在使用哪些组件
- 许可管理:控制谁可以获取高级或已授权组件
常见的身份验证模式
基于令牌的身份验证
最常用的方法是使用 Bearer 令牌或 API 密钥
{
"registries": {
"@private": {
"url": "https://registry.company.com/{name}.json",
"headers": {
"Authorization": "Bearer ${REGISTRY_TOKEN}"
}
}
}
}在环境变量中设置您的令牌
REGISTRY_TOKEN=your_secret_token_hereAPI 密钥身份验证
一些注册表使用请求头中的 API 密钥
{
"registries": {
"@company": {
"url": "https://api.company.com/registry/{name}.json",
"headers": {
"X-API-Key": "${API_KEY}",
"X-Workspace-Id": "${WORKSPACE_ID}"
}
}
}
}查询参数身份验证
对于更简单的设置,可以使用查询参数
{
"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 路由示例
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 示例
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)
})高级身份验证模式
基于团队的访问权限
为不同团队提供不同的组件
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
}多注册表身份验证
通过 命名空间注册表,您可以设置多个具有不同身份验证的注册表
{
"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}"
}
}
}
}这使您可以
- 混合使用公共和私有注册表
- 每个注册表使用不同的身份验证
- 按访问级别组织组件
安全最佳实践
使用环境变量
切勿将令牌提交到版本控制中。始终使用环境变量
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 }
)
}后续步骤
要通过多个注册表和高级模式设置身份验证,请参阅 命名空间注册表 文档。它涵盖了
- 设置多个已验证的注册表
- 在每个命名空间使用不同的身份验证
- 跨注册表依赖项解析
- 高级身份验证模式