1. 从零搭一个能跑的 Node 鉴权骨架先解决什么问题如果你正在写 Node 服务端迟早会撞上两件事一是用户登录后怎么发一个可信凭证二是密码到底怎么存才不心虚。前者就是 Token 的生成与验证后者是密码的加密与验证。这两个东西单独看都不难难的是把它们串成一条能跑通的链路注册时把明文密码变成哈希存库登录时比对哈希并签发 Token后续请求带着 Token 回来还能被正确解析出用户身份。我这次要交付的就是这么一套最小可跑骨架基于 Express Mongoose bcrypt jsonwebtoken passport-jwt。它不追求生产级完备但每一步都能复制、能验证、能看到结果。同时我会把 TaoToken 的统一 Key 通道接进来让模型调用和本地服务共用一套配置骨架settings.json 和 config.toml 两个片段都会给到。适合谁看刚接触 Node 服务端鉴权、想把注册登录流程跑通、又不想被一堆抽象概念绕晕的同学。你不需要先懂 JWT 的全部细节跟着敲完签发→校验→密码比对这条链路就能跑起来。核心检索词先摆出来Node Token 生成与验证、密码加密与验证、bcrypt 哈希、jsonwebtoken 签发、passport-jwt 校验、TaoToken 统一 Key。这几个词会贯穿全文。2. TaoToken 前置统一 Key 与 API 通道怎么接在写业务代码之前先把外部模型通道的配置骨架搭好。TaoToken 的作用是给你一个统一的 Key 和 API 入口本地服务、脚本、编辑器插件都可以复用同一套凭证不用每个工具单独配一遍。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置里直接写这个就行。你需要先去控制台拿一个 API Key入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。拿到之后本地配置分两种形态一种是给编辑器/工具用的 settings.json一种是给命令行/服务用的 config.toml。settings.json 片段长这样{ taotoken: { api_base: https://taotoken.net/api, api_key: sk-你的Key, default_model: claude-sonnet-4-20250514 } }config.toml 片段[taotoken] api_base https://taotoken.net/api api_key sk-你的Key default_model claude-sonnet-4-20250514这两个片段的作用是让 Node 服务在需要调用模型时从统一位置读取 Key而不是散落在各个文件里。你可以用 dotenv 把 api_key 注入环境变量再在代码里读 process.env.TAOTOKEN_API_KEY。提示Key 不要硬编码进提交到仓库的文件用 .env 加 .gitignore 隔离。settings.json 和 config.toml 里可以留占位符真实值走环境变量。如果你后面要做长期编码或 Agent 类任务可以了解下 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。模型对话调试入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。3. 可复制配置依赖清单与目录结构先把项目初始化依赖清单如下npm init -y npm install express mongoose bcrypt jsonwebtoken passport passport-jwt dotenv版本不用锁死装最新稳定版即可。bcrypt 是原生模块如果编译报错可以换 bcryptjsAPI 基本一致只是性能略低。目录结构建议这样node-auth-demo/ ├── config/ │ ├── keys.js │ └── passport.js ├── models/ │ └── User.js ├── routes/ │ └── api/ │ └── users.js ├── .env ├── server.js └── package.jsonconfig/keys.js 负责集中管理密钥module.exports { mongoURI: process.env.MONGO_URI || mongodb://127.0.0.1:27017/node_auth_demo, secretOrKey: process.env.JWT_SECRET || dev_secret_change_me };models/User.js 定义用户模型const mongoose require(mongoose); const Schema mongoose.Schema; const UserSchema new Schema({ name: { type: String, required: true }, email: { type: String, required: true }, password: { type: String, required: true }, avatar: { type: String }, date: { type: Date, default: Date.now } }); module.exports mongoose.model(users, UserSchema);server.js 把中间件、数据库、passport 初始化串起来require(dotenv).config(); const express require(express); const mongoose require(mongoose); const passport require(passport); const users require(./routes/api/users); const app express(); app.use(express.json()); app.use(express.urlencoded({ extended: false })); const dbUrl require(./config/keys).mongoURI; mongoose.connect(dbUrl) .then(() console.log(数据库连接成功)) .catch(err console.log(数据库连接失败, err)); app.use(passport.initialize()); require(./config/passport)(passport); app.use(/api/users, users); const port process.env.PORT || 5000; app.listen(port, () console.log(Server is running on port ${port}));到这里骨架就搭好了。接下来是核心注册时加密密码登录时校验密码并签发 Token。4. 密码加密与 Token 签发注册和登录路由注册路由的关键动作是先查邮箱是否已存在再用 bcrypt 生成盐并哈希密码最后存库。注意不要存明文也不要在日志里打印明文。routes/api/users.js 里的注册部分const express require(express); const router express.Router(); const bcrypt require(bcrypt); const User require(../../models/User); router.post(/register, (req, res) { User.findOne({ email: req.body.email }).then(user { if (user) { return res.status(400).json({ email: 邮箱已经被注册 }); } const newUser new User({ name: req.body.name, email: req.body.email, password: req.body.password }); bcrypt.genSalt(10, (err, salt) { if (err) throw err; bcrypt.hash(newUser.password, salt, (err, hash) { if (err) throw err; newUser.password hash; newUser.save() .then(user res.json(user)) .catch(err console.log(err)); }); }); }); });登录路由的关键动作是用 bcrypt.compare 比对明文和哈希匹配后用 jwt.sign 签发 Token返回时带上 Bearer 前缀。const jwt require(jsonwebtoken); const keys require(../../config/keys); router.post(/login, (req, res) { const email req.body.email; const password req.body.password; User.findOne({ email }).then(user { if (!user) { return res.status(404).json({ email: 邮箱不存在 }); } bcrypt.compare(password, user.password).then(isMatch { if (isMatch) { const rule { id: user.id, name: user.name, email: user.email }; jwt.sign(rule, keys.secretOrKey, { expiresIn: 3600 }, (err, token) { if (err) throw err; res.json({ success: true, token: Bearer token }); }); } else { return res.status(400).json({ password: 密码不匹配 }); } }); }); });这里 expiresIn 设成 3600 秒也就是一小时。生产环境可以短一些配合刷新机制。rule 里只放必要字段不要塞敏感信息因为 JWT 的 payload 是可解码的只是不能篡改。5. Token 验证passport-jwt 策略与受保护路由Token 签发出来只是第一步真正体现价值的是验证。passport-jwt 会从 Authorization 头里提取 Bearer Token用同一个 secretOrKey 验签验签通过后把 payload 交给回调回调里再查库确认用户存在。config/passport.jsconst JwtStrategy require(passport-jwt).Strategy; const ExtractJwt require(passport-jwt).ExtractJwt; const mongoose require(mongoose); const User mongoose.model(users); const keys require(../config/keys); const opts {}; opts.jwtFromRequest ExtractJwt.fromAuthHeaderAsBearerToken(); opts.secretOrKey keys.secretOrKey; module.exports passport { passport.use(new JwtStrategy(opts, (jwt_payload, done) { User.findById(jwt_payload.id) .then(user { if (user) { return done(null, user); } return done(null, false); }) .catch(err console.log(err)); })); };受保护路由 /current 挂在 users 路由下const passport require(passport); router.get(/current, passport.authenticate(jwt, { session: false }), (req, res) { res.json({ id: req.user.id, name: req.user.name, email: req.user.email }); });session: false 表示不用 session纯 Token 模式。passport.authenticate 验证失败会直接返回 401成功则把用户对象挂到 req.user 上。6. 验证请求curl 跑通签发→校验→密码比对全链路服务启动后用 curl 依次验证。先注册curl -X POST http://localhost:5000/api/users/register \ -H Content-Type: application/json \ -d {name:testuser,email:testexample.com,password:123456}返回用户对象注意 password 字段已经是哈希值不是明文。再登录curl -X POST http://localhost:5000/api/users/login \ -H Content-Type: application/json \ -d {email:testexample.com,password:123456}返回类似{success:true,token:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...}把 token 复制出来请求受保护路由curl http://localhost:5000/api/users/current \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...返回当前用户信息说明 Token 验证通过。如果不带 Token 或 Token 错误会返回 401 Unauthorized。密码比对这条链路在登录时已经隐式验证了输错密码会返回「密码不匹配」输对才签发 Token。你可以故意用错误密码登录一次确认 bcrypt.compare 返回 false。7. 本篇常见错排查报错一bcrypt 安装失败或 node-gyp 编译报错。这是原生模块常见问题。换 bcryptjs 即可代码里把 require(bcrypt) 改成 require(bcryptjs)API 完全兼容。或者升级 Node 版本和构建工具。报错二401 Unauthorized但 Token 明明是对的。检查三点Authorization 头格式是不是Bearer token中间有空格secretOrKey 签发和验证是不是同一个值Token 是不是过期了。expiresIn 3600 意味着一小时后失效重新登录拿新 Token。报错三Mongoose 连接超时。本地 MongoDB 没启动或者连接字符串写错。确认 mongod 在跑连接串用 127.0.0.1 而不是 localhost避免 IPv6 解析问题。报错四jwt.sign 回调里 err 被 throw 后进程退出。不要直接 throw改成 return res.status(500).json({ error: 签发失败 })避免未捕获异常打挂服务。报错五passport 策略没生效req.user 是 undefined。确认 server.js 里先 app.use(passport.initialize())再 require(./config/passport)(passport)顺序反了策略不会注册。报错六TaoToken 配置读取不到 Key。检查 .env 文件是否被 dotenv 加载process.env.TAOTOKEN_API_KEY 是否有值。settings.json 和 config.toml 里的占位符要替换成真实 Key或者确保环境变量注入正确。8. 配置骨架收尾与后续接入到这里签发→校验→密码比对这条链路已经跑通。你可以用同一套骨架继续扩展加刷新 Token、加角色权限、把 secretOrKey 换成非对称密钥。TaoToken 的统一 Key 通道则负责模型调用侧settings.json 和 config.toml 两个片段可以直接复制到你的工具链里。需要拿 Key 或看接入细节走这两个入口API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你在配 ClaudeCode 相关通道参考 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。实测下来最容易踩的坑不是 JWT 本身而是 bcrypt 的原生编译和 passport 的初始化顺序。把这两个搞定剩下的就是复制粘贴和 curl 验证。
