为什么需要用户设置和 API Key 管理?
前两篇完成了对话界面,但用户还需要管理自己的账号信息和 API Key。这是任何 SaaS 产品的标配功能。
本篇回答三个问题:
- 用户设置页面需要包含哪些功能?
- API Key 怎么生成和管理才安全?
- 用量统计怎么实现?
用户设置页面需要哪些功能?
基本信息编辑
用户登录后可以查看和编辑自己的昵称、头像、邮箱。
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