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

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

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

本篇回答三个问题:

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

用户设置页面需要哪些功能?

基本信息编辑

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

tsx 复制代码
// 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>
  );
}

修改密码

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

tsx 复制代码
// 在 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>

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

python 复制代码
# 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 是给第三方程序使用的认证方式。

数据库设计

sql 复制代码
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,只展示一次,之后只能看到前几位。

python 复制代码
# 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 管理页面

tsx 复制代码
// 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 认证中间件

python 复制代码
# 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 数、提问数。

sql 复制代码
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       -- 提问数
);

用量统计页面

tsx 复制代码
// 在 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 开发者社区 原文链接:www.zyentor.com/news/3820

相关推荐
小撒的私房菜1 小时前
Multi-Agent 里谁来指挥?我用一个调度员,让多个 Agent 开始协作
人工智能·后端·agent
不喝水就会渴1 小时前
【共创季稿事节】HarmonyOS 7.0 时代的新基建 :DevEco CLI + Claude Code,鸿蒙 AI 开发的黄金搭档
人工智能·华为·harmonyos
星河耀银海1 小时前
大模型和搜索引擎到底有什么不一样
人工智能·搜索引擎
沪漂阿龙1 小时前
《LangChain》成本、限流、缓存、降级:AI 应用上线要考虑的问题
人工智能·langchain
一切皆是因缘际会1 小时前
RLHF奖励坍塌:大模型Reward漂移机理
人工智能·数学建模·ai
阿庆_AI研发工程师1 小时前
从 OpenAI Codex 源码看生产级 AI Agent Runtime 的工程模式
人工智能
武子康1 小时前
调查研究-177 Agent / Harness 工具链研究:从会调用工具的 LLM,到可观测、可验证、可交付的智能体系统
人工智能
集芯微电科技有限公司1 小时前
四通道2A输出集成功率电感降压模块专为紧凑型方案设计
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
朱大喜2 小时前
NumPy 性能优化:内存布局、向量化与原地操作的实战经验
人工智能