1. 回顾:认证 vs 鉴权
上一篇我们讲了认证——解决"你是谁"的问题。用户登录后拿到 JWT,后续请求带上 token,服务端验证通过就知道你是谁。
但知道你是谁还不够。一个普通用户和一个管理员,能做的事是不一样的。鉴权就是在认证之后,检查"你有没有权限做这件事"。
认证回答的是身份问题,鉴权回答的是权限问题。两者的顺序不能反——你得先知道这个人是谁,才能去查他有什么权限。
2. 角色鉴权
最常见的鉴权方式是基于角色。比如:普通用户能查看自己的资料,但只有管理员能删除用户。
思路很简单:写一个中间件,从 JWT 的 payload 里取出角色,判断是否匹配。
01import { Hono } from 'hono'02import { jwt } from 'hono/jwt'03import type { JwtVariables } from 'hono/jwt'04import { HTTPException } from 'hono/http-exception'0506type Variables = JwtVariables0708const app = new Hono<{ Variables: Variables }>()09const SECRET = 'my-secret'1011// 角色鉴权中间件12const requireRole = (role: string) => {13// 这里为了先讲清流程,先省略 Hono 的中间件类型声明14return async (c, next) => {15const payload = c.get('jwtPayload')1617if (payload.role !== role) {18throw new HTTPException(403, { message: 'Forbidden: insufficient permissions' })19}2021await next()22}23}2425// 先认证26app.use('/api/*', jwt({ secret: SECRET, alg: 'HS256' }))2728// 所有登录用户都能访问29app.get('/api/profile', (c) => {30const payload = c.get('jwtPayload')31return c.json(payload)32})3334// 只有 admin 能删除用户35app.delete('/api/users/:id', requireRole('admin'), (c) => {36const id = c.req.param('id')37return c.json({ message: `User ${id} deleted` })38})3940// 只有 admin 能创建用户41app.post('/api/users', requireRole('admin'), async (c) => {42const body = await c.req.json()43return c.json({ message: 'User created', data: body })44})4546export default app
关键点:requireRole 是一个高阶函数,传入角色名,返回中间件。它必须放在 jwt() 之后,因为需要先解析出 jwtPayload 才能检查角色。
这就是认证和鉴权在代码里的落地顺序:
jwt()先确认"你是谁"requireRole()再确认"你有没有这个权限"
如果顺序写反了,鉴权中间件根本拿不到用户身份信息。
3. 权限列表鉴权
角色鉴权够用的场景是:权限和角色一一对应,比如"只有 admin 能做 X"。但如果权限更细粒度,比如同样是 admin,有的能删用户但不能改配置,那就需要权限列表。
思路是在 JWT 的 payload 里带上一个 permissions 数组,鉴权时检查这个数组里有没有对应权限:
01const requirePermission = (permission: string) => {02// 这里同样先省略类型声明,重点看权限判断逻辑03return async (c, next) => {04const payload = c.get('jwtPayload')05const permissions: string[] = payload.permissions || []0607if (!permissions.includes(permission)) {08throw new HTTPException(403, { message: `Missing permission: ${permission}` })09}1011await next()12}13}1415// 需要 users:delete 权限16app.delete('/api/users/:id', requirePermission('users:delete'), (c) => {17const id = c.req.param('id')18return c.json({ message: `User ${id} deleted` })19})2021// 需要 config:write 权限22app.put('/api/config', requirePermission('config:write'), async (c) => {23const body = await c.req.json()24return c.json({ message: 'Config updated', data: body })25})
签发 token 时把权限写进去:
01const token = await sign(02{03sub: user.id,04email: user.email,05role: user.role,06permissions: ['users:read', 'users:delete', 'config:read'],07exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24,08},09SECRET10)
角色鉴权和权限列表鉴权怎么选?
- 权限模型简单(admin/user 两种角色就够了)→ 用角色鉴权,代码最少
- 权限模型复杂(不同角色有不同的细粒度操作权限)→ 用权限列表
4. API Key 鉴权
不是所有场景都适合 JWT。JWT 是给"用户登录"设计的——有签发、有过期、有身份信息。但有些接口不是给用户调的,而是给其他程序调的,比如:
- 微服务之间互相调用
- 第三方系统接入你的 API
- 后台定时任务请求内部接口
这些场景不存在"用户登录"这个动作,用 JWT 反而多此一举。API Key 更简单直接:一个固定的字符串,请求时带上,服务端对比一下就行。
01import { Hono } from 'hono'0203type Bindings = {04API_KEY: string05}0607const app = new Hono<{ Bindings: Bindings }>()0809// API Key 鉴权中间件10// 这里先省略中间件类型声明,先理解 API Key 的校验流程11const apiKeyAuth = async (c, next) => {12const apiKey = c.req.header('X-API-Key')1314if (!apiKey || apiKey !== c.env.API_KEY) {15return c.json({ error: 'Invalid API Key' }, 401)16}1718await next()19}2021// 公开接口22app.get('/health', (c) => {23return c.json({ status: 'ok' })24})2526// 需要 API Key 的接口27app.use('/internal/*', apiKeyAuth)2829app.get('/internal/stats', (c) => {30return c.json({ totalUsers: 1000, activeToday: 200 })31})3233export default app
5. JWT vs API Key 怎么选
如果你是第一次做后端,可以先记一个最实用的判断:
| JWT | API Key | |
|---|---|---|
| 适用场景 | 用户登录后的接口 | 程序调用程序的接口 |
| 身份信息 | token 自带用户 ID、角色等 | 只能证明"你有 Key",不带身份 |
| 有效期 | 有过期时间,需要刷新 | 通常长期有效,手动轮换 |
| 传递方式 | Authorization: Bearer <token> | X-API-Key: <key> 或其他自定义头 |
简单总结:
- 面向"登录用户"的接口 → JWT
- 面向"程序调用程序"的接口 → API Key
6. 总结
鉴权解决的是"你能干什么"的问题。核心思路就是在认证之后加一层中间件做权限检查:
- 角色鉴权:从 JWT payload 取
role,判断是否匹配。适合权限模型简单的场景 - 权限列表鉴权:从 JWT payload 取
permissions数组,检查是否包含所需权限。适合细粒度控制 - API Key:适合服务间调用,不走 JWT 那套流程,直接对比字符串
不管哪种方式,鉴权中间件都必须放在认证中间件之后——先知道你是谁,再检查你能做什么。