认证与授权的区别
要点
- 认证(Authentication)回答「你是谁」,授权(Authorization)回答「你能做什么」
- 认证是授权的前提,但两者在实现上应该解耦
- AI 项目里认证通常是用户身份、API Key、服务账号;授权通常是模型配额、功能开关、租户隔离
- 把认证和授权混在一起写,后期加权限模型或换认证方式时会牵一发动全身
- Hono 的中间件体系可以把认证和授权拆成独立层,职责清晰
1. 两个概念
认证(Authentication,简称 AuthN)是验证身份的过程:
- 用户输入账号密码,系统确认是本人
- 请求带上 API Key,系统识别是哪个开发者
- 服务之间用 mTLS,双方确认身份
授权(Authorization,简称 AuthZ)是判断已认证的主体能做什么:
- 普通用户不能访问管理接口
- 免费用户每月只能用 10 次 GPT-4
- 租户 A 不能读取租户 B 的数据
两者关系:先认证,后授权。认证失败直接拒绝,认证通过再检查权限。
2. 实现层面的区别
2.1 认证的实现
认证的核心是「证明你是谁」,常见方式:
| 方式 | 场景 | 凭据 |
|---|---|---|
| 用户名+密码 | Web 登录 | 密码 |
| JWT | 无状态 API | Token |
| Session | 有状态 Web | Cookie |
| API Key | 开发者调用 | Key |
| OAuth | 第三方登录 | Access Token |
| mTLS | 服务间调用 | 证书 |
认证的结果是得到一个「身份」,通常放在请求上下文里:
// 认证中间件
app.use('*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
try {
const payload = await verifyJWT(token)
c.set('user', payload) // 把身份放到上下文
} catch {
return c.json({ error: 'Invalid token' }, 401)
}
await next()
})2.2 授权的实现
授权的核心是「检查你能做什么」,常见模型:
| 模型 | 适用场景 | 示例 |
|---|---|---|
| RBAC | 角色明确 | 管理员、普通用户 |
| ABAC | 规则复杂 | 租户 A 的 VIP 用户可以访问 |
| ACL | 资源粒度 | 用户 X 可以读资源 Y |
| ReBAC | 关系驱动 | 团队成员可以查看团队文档 |
授权通常在路由层或业务层检查:
// 授权中间件
function requireRole(role: string) {
return async (c: Context, next: Next) => {
const user = c.get('user')
if (user.role !== role) {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
}
// 使用
app.post('/admin/delete', requireRole('admin'), async (c) => {
// 只有 admin 角色能访问
})3. 为什么要解耦
把认证和授权混在一起,常见的问题是:
3.1 换认证方式时牵连授权逻辑
// ❌ 耦合:认证和授权写在一起
app.use('*', async (c, next) => {
// 从 cookie 里读 session
const session = await getSession(c.req.header('Cookie'))
if (!session) return c.json({ error: 'Unauthorized' }, 401)
// 直接检查权限
if (session.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
})如果要从 Session 切换到 JWT,授权逻辑也要改。
3.2 加权限模型时改动大
// ❌ 硬编码权限
if (user.role === 'admin' || user.role === 'editor') {
// 允许编辑
}如果要从 RBAC 切换到 ABAC,所有硬编码的地方都要改。
3.3 解耦的方式
认证和授权分层:
// 第一层:认证(只负责身份)
app.use('*', authMiddleware())
// 第二层:授权(只负责权限)
app.post('/articles', requirePermission('article:create'), async (c) => {
// ...
})
// 第三层:资源级授权
app.put('/articles/:id', requireOwnership('article'), async (c) => {
// ...
})认证中间件只关心「谁在请求」,授权中间件只关心「能做什么」。
4. AI 项目的认证授权特点
AI 项目的认证授权有一些特殊需求:
4.1 多维度授权
不只是「能不能访问」,还有「能用多少」:
- 模型配额:免费用户每月 100 次,付费用户 10000 次
- 功能开关:只有 Pro 用户能用 GPT-4
- 并发限制:每个租户最多 10 个并发请求
// 多维度授权
async function checkQuota(userId: string, model: string) {
const usage = await getUsage(userId, model)
const quota = await getQuota(userId, model)
if (usage.count >= quota.maxCount) {
throw new QuotaExceededError('Monthly limit reached')
}
if (usage.tokens >= quota.maxTokens) {
throw new QuotaExceededError('Token limit reached')
}
}4.2 多租户隔离
SaaS 场景下,租户之间的数据必须隔离:
// 租户隔离
app.use('/api/*', tenantMiddleware())
app.get('/conversations', async (c) => {
const tenantId = c.get('tenantId')
// 只能查自己的数据
const conversations = await db
.select()
.from(conversations)
.where(eq(conversations.tenantId, tenantId))
})4.3 服务账号
AI 服务之间调用也需要认证:
- Worker 调用 LLM API
- RAG 服务调用向量数据库
- 定时任务调用业务接口
服务账号通常用 API Key 或 mTLS,不走用户认证流程。
5. Hono 里的实现
Hono 的中间件体系适合分层实现认证授权:
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// 第一层:认证
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET! }))
// 第二层:角色授权
const requireRole = (role: string) =>
async (c: Context, next: Next) => {
const user = c.get('jwtPayload')
if (user.role !== role) {
throw new HTTPException(403, { message: 'Forbidden' })
}
await next()
}
// 第三层:权限检查
const requirePermission = (permission: string) =>
async (c: Context, next: Next) => {
const user = c.get('jwtPayload')
const hasPermission = await checkPermission(user.id, permission)
if (!hasPermission) {
throw new HTTPException(403, { message: 'No permission' })
}
await next()
}
// 使用
app.post('/articles',
requireRole('editor'),
requirePermission('article:create'),
async (c) => {
// 创建文章
}
)6. 常见错误
6.1 认证失败返回 403
认证失败应该返回 401 Unauthorized,不是 403 Forbidden:
// ❌ 错误
if (!token) return c.json({ error: 'Forbidden' }, 403)
// ✅ 正确
if (!token) return c.json({ error: 'Unauthorized' }, 401)- 401:未认证(你是谁?)
- 403:已认证但没权限(你不能做什么)
6.2 授权检查放在业务逻辑里
// ❌ 授权和业务逻辑混在一起
app.post('/articles', async (c) => {
const user = c.get('user')
// 先检查权限
if (user.role !== 'editor') {
return c.json({ error: 'Forbidden' }, 403)
}
// 再执行业务
await createArticle(...)
})应该用中间件或装饰器把授权检查抽出来:
// ✅ 授权和业务分离
app.post('/articles', requireRole('editor'), async (c) => {
await createArticle(...)
})6.3 忽略租户隔离
// ❌ 没有租户隔离
app.get('/conversations', async (c) => {
const conversations = await db.select().from(conversations)
// 返回所有租户的数据!
})
// ✅ 租户隔离
app.get('/conversations', async (c) => {
const tenantId = c.get('tenantId')
const conversations = await db
.select()
.from(conversations)
.where(eq(conversations.tenantId, tenantId))
})总结
认证是「你是谁」,授权是「你能做什么」。两者在概念和实现上都应该解耦。
这一节涉及到的几个要点:
- 认证 vs 授权:认证验证身份,授权检查权限
- 解耦:认证和授权分层实现,互不耦合
- AI 项目特点:多维度授权、多租户隔离、服务账号
- Hono 实现:中间件分层,职责清晰
- 常见错误:状态码用错、授权和业务耦合、忽略租户隔离
认证和授权是系统安全的基石。分层实现、职责清晰,后期扩展才不会牵一发动全身。
下一篇看用户注册与登录——注册流程、登录流程、输入校验、错误处理。