认证、授权、JWT、密码哈希:Node.js 鉴权到底在做什么
很多人第一次做登录系统时,都会遇到同一个问题:
"我已经拿到用户名和密码了,接下来到底该怎么设计?"
如果把这件事说清楚,其实就四步:
- 用户注册时,把密码安全地存起来
- 用户登录时,校验密码是否正确
- 登录成功后,发一个身份凭证给客户端
- 后续请求里,检查这个凭证是否有效
这篇先不急着写代码,先把整个链路讲明白。因为只要概念清楚了,后面用 bcryptjs 和 jose 写实现就会很顺。
1. 先分清三个词:认证、授权、鉴权
这三个词经常被混着说,但它们不是一回事。
认证(Authentication)
回答的是:"你是谁?"
比如:
- 用户输入账号密码登录
- 服务端判断这个人是不是这个账号的主人
授权(Authorization)
回答的是:"你能做什么?"
比如:
- 普通用户只能看自己的订单
- 管理员才能删除用户
鉴权
中文里很多项目会把"认证 + 授权"统称为"鉴权"。
所以你看到"鉴权系统",通常不是单纯验密码,而是一整套登录和权限控制机制。
2. 一个登录系统最小闭环长什么样
最基本的流程是这样的:
注册
用户输入用户名和密码。
服务端不能直接把密码明文存数据库,而是要做哈希处理后再存。
登录
用户再次输入用户名和密码。
服务端拿用户输入的密码,和数据库里保存的哈希值做比对。
比对通过,说明密码正确。
签发身份凭证
登录成功后,服务端给客户端发一个 token。
这个 token 之后会被客户端带着去访问受保护接口。
访问受保护接口
客户端请求时,把 token 带上。
服务端检查 token 是否有效、是否过期、是否被篡改。
通过后,才允许访问。
这就是一个最小闭环。
3. 为什么密码不能明文存储
这是最重要的一点之一。
如果数据库里直接存的是明文密码,一旦数据库泄漏,后果非常严重。
因为很多人会在多个网站重复使用同一个密码。
所以正确做法不是"加密密码",而是"哈希密码"。
哈希不是加密
两者差别很大:
- 加密:可以解密回来
- 哈希:通常不能反推出原文
密码场景里更适合哈希,因为系统根本不需要知道你的原密码是什么,只需要在登录时验证"你输入的密码,和当初那个密码是不是同一个"。
为什么不能只用普通哈希函数
像 md5、sha1 这类算法已经不适合拿来存密码了。
原因很简单:
- 速度太快,暴力破解成本低
- 容易被彩虹表攻击
- 对密码场景缺少足够的抗暴力破解能力
所以密码哈希通常要用专门的慢哈希算法,比如 bcrypt。
4. bcryptjs 是做什么的
bcryptjs 的作用很直接:
- 注册时:把明文密码变成哈希
- 登录时:把用户输入的密码和哈希结果比对
它的核心价值有两个:
1)加入 salt
salt 可以理解为给每个密码加一段随机扰动。
同样的密码,每次生成的哈希结果也会不同。
这能避免很多常见攻击方式。
2)增加计算成本
bcrypt 是慢哈希。
它故意让计算变慢,目的是提高暴力破解的成本。
这不是缺点,而是设计目标。
5. JWT 是什么
JWT 是 "JSON Web Token"。
它常被用来做登录后的身份凭证。
你可以把它理解成一张"电子通行证":
- 登录成功后,服务端签发给你
- 之后你访问受保护接口时,带着它
- 服务端检查它是不是自己签的、有没有过期
JWT 不是数据库
JWT 本身不是一个会话记录表。
它通常是无状态的:服务端不一定需要保存会话,就能验证这个 token。
JWT 也不是加密串
很多人以为 JWT 是加密后的数据,其实不准确。
JWT 里 payload 部分只是编码过,不是保密的。
任何人都可以把 token 拿去解码看到里面的内容。
真正保证可信的是"签名"。
所以 JWT 的重点是:
- 你能看见内容
- 但你不能篡改内容
6. JWT 的结构
一个 JWT 通常由三段组成:
text
header.payload.signature
Header
说明算法、类型等信息。
Payload
放业务数据,比如:
- 用户 id
- 角色
- 过期时间相关字段
Signature
签名部分,用来防篡改。
如果有人改了 payload 里的内容,签名就对不上,服务端会直接判定 token 无效。
7. jose 是做什么的
jose 是一个处理 JWT、JWS、JWE 的库。
在常见登录场景里,你最常用到的是:
- 签发 JWT
- 校验 JWT
也就是说:
bcryptjs负责"密码"jose负责"token"
这两者分工非常明确。
8. 一次完整请求链路长什么样
可以把整个过程想成下面这个流程:
第一步:注册
用户提交密码。
服务端:
- 校验参数
- 用
bcryptjs哈希密码 - 把哈希值存入数据库
第二步:登录
用户提交账号和密码。
服务端:
- 根据账号查用户
- 取出数据库里的密码哈希
- 用
bcryptjs.compare()比对 - 通过后,用
jose签发 JWT
第三步:访问接口
客户端请求接口时,把 JWT 带上。
服务端:
- 取出 token
- 用
jose验签 - 检查是否过期
- 通过后把用户信息挂到请求上下文里
第四步:做权限判断
比如:
- 普通用户只能看自己的资料
- 管理员才能进后台
这一步才是真正的"授权"。
9. 常见误区
误区 1:JWT 就是安全
不对。JWT 只是一个身份凭证格式。
它是否安全,取决于:
- 密钥是否足够强
- 是否正确验签
- 是否设置过期时间
- 是否做好密钥管理
误区 2:JWT 是加密的
不对。JWT 默认不是加密,只是签名。
payload 能被解码看到。
误区 3:有了 JWT 就不用管权限
不对。
JWT 只能证明"这个请求带着一个合法 token"。
至于"这个人能不能删用户、能不能进后台",还得单独做权限判断。
误区 4:密码用 md5 就够了
不够。
密码哈希要选适合密码场景的方案,bcrypt 就是常见选择之一。
10. 这套方案里两个库各管什么
可以直接记成一句话:
bcryptjs管密码jose管 token
再展开一点:
bcryptjs
- 注册时哈希密码
- 登录时校验密码
jose
- 登录成功后签发 JWT
- 后续请求时验证 JWT
11. 这一篇你应该建立的核心认知
如果只记住三件事,就够了:
- 密码不能明文存,必须哈希
- JWT 是登录后的身份凭证,不是数据库,不是加密数据
bcryptjs和jose分别解决的是两类不同问题
12. 下一篇该讲什么
接下来最适合单独展开的,是密码哈希这部分。
因为一旦注册和登录的密码处理讲透了,后面再看 JWT 和权限控制就会很自然。
下一篇就可以专门讲:
- 为什么 bcrypt 适合密码
- salt 和 cost factor 是什么
- 注册和登录时分别怎么写
- 常见坑有哪些
如果你后面要继续,我就按这个系列往下写第二篇。
后记
2026年5月7日于上海,在claude opus 4.6辅助下完成。