欢迎光临
我们一直在努力

AI 全栈开发实战(9):用户设置与 API Key 管理——账号安全与用量统计

为什么需要用户设置和 API Key 管理?

前两篇完成了对话界面,但用户还需要管理自己的账号信息和 API Key。这是任何 SaaS 产品的标配功能。

本篇回答三个问题:

  • 用户设置页面需要包含哪些功能?
  • API Key 怎么生成和管理才安全?
  • 用量统计怎么实现?
  • 用户设置页面需要哪些功能?

    基本信息编辑

    用户登录后可以查看和编辑自己的昵称、头像、邮箱。

    // frontend/src/pages/Settings.tsx
    import { useState, useEffect } from "react";
    import { useAuth } from "@/hooks/useAuth";
    import api from "@/lib/api";
    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

    export default function Settings() {
    const { user, token } = useAuth();
    const [nickname, setNickname] = useState(user?.nickname || "");
    const [email, setEmail] = useState(user?.email || "");
    const [saving, setSaving] = useState(false);
    const [message, setMessage] = useState("");

    const handleSave = async () => {
    setSaving(true);
    try {
    await api.put("/auth/profile", { nickname, email });
    setMessage("保存成功");
    } catch (e: any) {
    setMessage(e.response?.data?.detail || "保存失败");
    }
    setSaving(false);
    setTimeout(() => setMessage(""), 3000);
    };

    return (
    <div className="max-w-2xl mx-auto px-4 py-8 space-y-6">
    <h1 className="text-2xl font-bold">账号设置</h1>

    <Card>
    <CardHeader><CardTitle>基本信息</CardTitle></CardHeader>
    <CardContent className="space-y-4">
    <div>
    <label className="text-sm font-medium mb-1 block">昵称</label>
    <Input value={nickname} onChange={(e) => setNickname(e.target.value)} />
    </div>
    <div>
    <label className="text-sm font-medium mb-1 block">邮箱</label>
    <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
    </div>
    {message && (
    <p className={`text-sm ${message === "保存成功" ? "text-green-600" : "text-red-600"}`}>
    {message}
    </p>
    )}
    <Button onClick={handleSave} disabled={saving}>
    {saving ? "保存中…" : "保存"}
    </Button>
    </CardContent>
    </Card>
    </div>
    );
    }

    修改密码

    安全敏感操作需要独立的密码修改流程。

    // 在 Settings.tsx 中增加密码修改卡片
    <Card>
    <CardHeader><CardTitle>修改密码</CardTitle></CardHeader>
    <CardContent className="space-y-4">
    <Input type="password" placeholder="当前密码" value={oldPwd}
    onChange={(e) => setOldPwd(e.target.value)} />
    <Input type="password" placeholder="新密码(至少 6 位)" value={newPwd}
    onChange={(e) => setNewPwd(e.target.value)} />
    <Input type="password" placeholder="确认新密码" value={confirmPwd}
    onChange={(e) => setConfirmPwd(e.target.value)} />
    <Button onClick={handleChangePwd}>修改密码</Button>
    </CardContent>
    </Card>

    后端需要验证旧密码正确性,再更新为新密码。

    # backend/app/routers/auth.py(追加)
    @router.put("/auth/password")
    async def change_password(
    body: ChangePasswordRequest,
    user: User = Depends(require_auth),
    db: AsyncSession = Depends(get_db),
    ):
    """修改密码。"""
    if not verify_password(body.old_password, user.password_hash):
    raise HTTPException(400, "当前密码错误")
    if len(body.new_password) < 6:
    raise HTTPException(400, "新密码至少 6 位")
    user.password_hash = hash_password(body.new_password)
    await db.commit()
    return {"status": "ok"}

    API Key 怎么生成和管理?

    为什么需要 API Key?

    当用户需要通过编程方式调用 KNow 的问答接口(比如集成到自己的工具链中),网页登录的方式就不适用了。API Key 是给第三方程序使用的认证方式。

    数据库设计

    CREATE TABLE api_keys (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES users(id),
    key_hash TEXT NOT NULL, — 存储哈希,不存明文
    name TEXT DEFAULT '', — key 的名称(让用户区分用途)
    last_used_at INTEGER,
    is_active INTEGER DEFAULT 1,
    created_at INTEGER DEFAULT (strftime('%s','now'))
    );

    为什么存哈希不存明文? 跟密码一样,API Key 是敏感凭证。如果数据库泄露,明文 Key 会导致所有用户的数据暴露。存哈希的话泄露了也无法使用。

    API Key 生成与展示

    用户创建 Key 时,服务器生成一个唯一 Key,只展示一次,之后只能看到前几位。

    # backend/app/services/api_key_service.py
    import secrets
    import hashlib

    def generate_api_key() > str:
    """生成 API Key:sk_前缀 + 32位随机字符串。"""
    return f"sk_{secrets.token_hex(32)}"

    def hash_api_key(key: str) > str:
    """对 API Key 做单向哈希。"""
    return hashlib.sha256(key.encode()).hexdigest()

    def validate_api_key(key: str) > str:
    """验证 API Key 格式。"""
    if not key.startswith("sk_"):
    raise ValueError("API Key 格式错误")
    return hash_api_key(key)

    API Key 管理页面

    // frontend/src/pages/ApiKeys.tsx
    import { useState, useEffect } from "react";
    import api from "@/lib/api";
    import { Button } from "@/components/ui/button";
    import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
    import { Input } from "@/components/ui/input";

    interface ApiKey {
    id: number;
    name: string;
    prefix: string;
    last_used_at: string | null;
    is_active: boolean;
    created_at: string;
    }

    export default function ApiKeys() {
    const [keys, setKeys] = useState<ApiKey[]>([]);
    const [name, setName] = useState("");
    const [newKey, setNewKey] = useState("");

    useEffect(() => { loadKeys(); }, []);

    const loadKeys = async () => {
    const { data } = await api.get("/api-keys");
    setKeys(data);
    };

    const handleCreate = async () => {
    const { data } = await api.post("/api-keys", { name });
    setNewKey(data.key); // 只展示这一次
    loadKeys();
    setName("");
    };

    const handleDelete = async (id: number) => {
    await api.delete(`/api-keys/${id}`);
    loadKeys();
    };

    return (
    <div className="max-w-2xl mx-auto px-4 py-8 space-y-6">
    <h1 className="text-2xl font-bold">API Key 管理</h1>

    {/* 新建 Key */}
    <Card>
    <CardHeader><CardTitle>创建 API Key</CardTitle></CardHeader>
    <CardContent className="space-y-3">
    <Input placeholder="名称(如:开发环境)" value={name}
    onChange={(e) => setName(e.target.value)} />
    <Button onClick={handleCreate}>创建</Button>
    </CardContent>
    </Card>

    {/* 新 Key 提示(只出现一次) */}
    {newKey && (
    <Card className="border-yellow-300 bg-yellow-50">
    <CardContent className="pt-4">
    <p className="text-sm font-medium text-yellow-800">新 Key 已创建,请立即保存!</p>
    <p className="text-xs text-yellow-600 mt-1">关闭后不会再显示完整 Key</p>
    <Input value={newKey} readOnly className="mt-2 font-mono text-xs" />
    </CardContent>
    </Card>
    )}

    {/* Key 列表 */}
    <Card>
    <CardHeader><CardTitle>已有 Key</CardTitle></CardHeader>
    <CardContent className="space-y-3">
    {keys.map((k) => (
    <div key={k.id} className="flex items-center justify-between py-2 border-b last:border-0">
    <div>
    <p className="text-sm font-medium">{k.name}</p>
    <p className="text-xs text-gray-400 font-mono">{k.prefix}…</p>
    <p className="text-xs text-gray-400">{k.last_used_at ? "最近使用" : "未使用"}</p>
    </div>
    <Button variant="outline" size="sm" onClick={() => handleDelete(k.id)}>
    删除
    </Button>
    </div>
    ))}
    </CardContent>
    </Card>
    </div>
    );
    }

    API Key 认证中间件

    # backend/app/services/api_key_auth.py
    from app.services.api_key_service import hash_api_key

    async def authenticate_by_api_key(request: Request):
    """API Key 认证中间件。"""
    auth_header = request.headers.get("Authorization", "")
    if not auth_header.startswith("Bearer sk_"):
    return None

    key_hash = hash_api_key(auth_header[7:])
    with models.get_db() as conn:
    row = conn.execute(
    "SELECT user_id FROM api_keys WHERE key_hash=? AND is_active=1",
    (key_hash,)
    ).fetchone()
    if row:
    # 更新最后使用时间
    conn.execute(
    "UPDATE api_keys SET last_used_at=? WHERE key_hash=?",
    (int(time.time()), key_hash)
    )
    conn.commit()
    return row["user_id"]
    return None

    用量统计怎么实现?

    统计什么

    每个用户每天调用 API 的次数、消耗的 Token 数、提问数。

    CREATE TABLE usage_stats (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES users(id),
    date TEXT NOT NULL, — 2026-06-18
    api_calls INTEGER DEFAULT 0, — API 调用次数
    tokens_used INTEGER DEFAULT 0, — 消耗 Token 数
    queries INTEGER DEFAULT 0 — 提问数
    );

    用量统计页面

    // 在 Settings.tsx 中增加用量统计卡片
    <Card>
    <CardHeader><CardTitle>用量统计</CardTitle></CardHeader>
    <CardContent>
    {loading ? (
    <p className="text-sm text-gray-400">加载中…</p>
    ) : (
    <div className="grid grid-cols-3 gap-4">
    <div className="text-center p-3 bg-blue-50 rounded-xl">
    <p className="text-2xl font-bold text-blue-600">{stats.api_calls}</p>
    <p className="text-xs text-gray-500 mt-1">API 调用</p>
    </div>
    <div className="text-center p-3 bg-green-50 rounded-xl">
    <p className="text-2xl font-bold text-green-600">{stats.tokens_used}</p>
    <p className="text-xs text-gray-500 mt-1">Token 消耗</p>
    </div>
    <div className="text-center p-3 bg-purple-50 rounded-xl">
    <p className="text-2xl font-bold text-purple-600">{stats.queries}</p>
    <p className="text-xs text-gray-500 mt-1">提问次数</p>
    </div>
    </div>
    )}
    </CardContent>
    </Card>

    总结

    功能和对应解决的问题:

    功能解决什么问题
    修改昵称/邮箱 用户信息管理
    修改密码 账号安全
    API Key 管理 第三方程序调用接口
    用量统计 直观了解使用情况

    下一篇将进入部署阶段——用 Docker 打包 KNow 并发布到服务器。


    本文是 《AI 全栈开发实战——做一个真正的产品》 系列的第 9 篇。 本文由 Zyentor(智元界) 原创发布


    本文发布于 Zyentor(智元界) —— AI 开发者社区 原文链接:https://www.zyentor.com/news/3820

    赞(0)
    未经允许不得转载:171主机测评 » AI 全栈开发实战(9):用户设置与 API Key 管理——账号安全与用量统计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址