• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

给AI应用加上用户系统:JWT + 邮箱验证从零实现(附可直接运行的代码)

AI Coding 实测 admin 1小时前 5次浏览 已收录 扫描二维码

给AI应用加上用户系统:JWT + 邮箱验证从零实现(附可直接运行的代码)

为什么AI应用不能跳过头等大事

很多人做一个AI产品,第一版就是:前端一个输入框,后端一个调用大模型的接口,完事。用户进来,API key焊死在代码里,谁都能调。等到有人真用了,才发现连”这是谁在调用”都分不清。

用户系统不是可有可无的加分项。你要限流、要计费、要区分免费和付费用户、要做使用量统计,前提都是”知道每个请求是谁发的”。没有身份层,这些全做不了。而且现在主流大模型API按量计费,一个没有认证的接口,等于把钱包敞开给全网。

这篇文章给你一套能跑的方案:JWT做令牌,邮箱验证码做注册,不需要手机号,不需要第三方登录SDK,一个数据库两张表,Node.js版,10分钟内能落地。

你需要准备什么

  • Node.js 18+
  • 一个PostgreSQL数据库(本地或Supabase/Neon免费档都够用)
  • 一个能发邮件的服务(Resend、Postmark,或者先用控制台打印验证码本地测试)
  • npm装三个包:expressjsonwebtokenbcryptjs,再加一个Postgres客户端

这套思路跟语言无关。用Python的FastAPI、Go的Gin,原理一模一样,只是库名换一下。

数据库表设计:两张表解决问题

先建两张表。第一张存用户,第二张存邮箱验证码。分开存的原因是验证码有生命周期,到期要清掉,混在用户表里会很乱。

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE email_verifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL,
  code TEXT NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL,
  used BOOLEAN DEFAULT false
);

关键点:邮箱存成小写,密码只存哈希(bcrypt),验证码一定要有过期时间。这三个是新手最容易踩的坑。

注册流程:先验证码再建账号

标准流程是:用户填邮箱和密码 → 后端发一封带验证码的邮件 → 用户填验证码 → 验证通过才创建账户。这样能挡住大量机器人注册,也能确认邮箱是真实存在的。

const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET; // 务必用环境变量,别硬编码

// 1. 请求验证码
app.post('/api/auth/request-code', async (req, res) => {
  const email = req.body.email.toLowerCase();
  const code = crypto.randomInt(100000, 999999).toString();
  await db.query(
    `INSERT INTO email_verifications (email, code, expires_at)
     VALUES ($1, $2, now() + interval '10 minutes')`,
    [email, code]
  );
  await sendEmail(email, `你的验证码是 ${code},10分钟内有效`);
  res.json({ ok: true });
});

// 2. 验证码 + 密码 → 创建用户
app.post('/api/auth/register', async (req, res) => {
  const { email, code, password } = req.body;
  const lower = email.toLowerCase();
  const row = await db.query(
    `SELECT code FROM email_verifications
     WHERE email = $1 AND used = false AND expires_at > now()
     ORDER BY expires_at DESC LIMIT 1`, [lower]
  );
  if (!row.rows.length || row.rows[0].code !== code) {
    return res.status(400).json({ error: '验证码无效或已过期' });
  }
  const hash = await bcrypt.hash(password, 10);
  const user = await db.query(
    `INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id`,
    [lower, hash]
  );
  await db.query(`UPDATE email_verifications SET used = true WHERE email = $1`, [lower]);
  const token = jwt.sign({ sub: user.rows[0].id }, JWT_SECRET, { expiresIn: '7d' });
  res.json({ token });
});

验证码设置10分钟有效期是我从多个邮件服务商的实践里看到的主流做法。太短用户来不及看邮箱,太长安全性下降。10分钟是个比较好的平衡点。

登录与鉴权中间件

注册完登录就简单了,比对密码哈希,签发JWT。关键是后面这个中间件,它负责解析令牌、把用户身份挂到请求上,这就是”每个请求是谁发的”的答案。

app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const lower = email.toLowerCase();
  const user = await db.query(
    `SELECT id, password_hash FROM users WHERE email = $1`, [lower]
  );
  if (!user.rows.length ||
      !await bcrypt.compare(password, user.rows[0].password_hash)) {
    return res.status(401).json({ error: '邮箱或密码错误' });
  }
  const token = jwt.sign({ sub: user.rows[0].id }, JWT_SECRET, { expiresIn: '7d' });
  res.json({ token });
});

// 鉴权中间件:所有需要登录的接口套上它
function requireAuth(req, res, next) {
  const header = req.headers.authorization || '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: '未登录' });
  try {
    const payload = jwt.verify(token, JWT_SECRET);
    req.userId = payload.sub;
    next();
  } catch {
    return res.status(401).json({ error: '令牌无效或已过期' });
  }
}

// 示例:限流的调用接口
app.post('/api/chat/completions', requireAuth, async (req, res) => {
  // req.userId 就是当前用户,这里可以做限流/计费/统计
  // ... 调用大模型
});

前端拿到token后,每次请求在请求头里带 Authorization: Bearer 就行。用axios的话一行:

axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;

三个必须现在就想清楚的事

1. token存哪里。 存localStorage最简单,但有XSS风险。存HttpOnly Cookie更安全,但要处理CSRF。个人项目可以先localStorage跑起来,等有真实用户量了再升级。我的建议是别纠结,先能跑,安全层后面补。

2. 密码强度。 bcrypt的cost参数默认10,够用。别自己写加密算法,用成熟的库。这个坑踩过的人多了去了。

3. 限流。 有了用户系统,下一步就是给每个用户加使用限制。免费用户每天调多少次、每分钟几次,这个表结构要提前留好字段(比如在users表加 tokens_usedplan 列),不然后面加会很痛苦。

收尾

到这里你已经有了一套能用的用户系统:注册带着邮箱验证码,登录签发JWT,中间件保护所有需要身份的接口。这套东西不依赖任何第三方登录SDK,代码全在你手里,想怎么改都行。

下一步的自然延伸是费率计划和用量统计,那属于商业化的范畴了。先把身份这层地基打好,后面的楼才能盖得稳。

如果你卡在某个环节,多半是数据库连不上或者环境变量没配——这两类是独立开发者做认证最常遇到的两个问题,排查顺序从后往前看。

喜欢 (0)
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。