认证与授权的区别

要点

  • 认证(Authentication)回答「你是谁」,授权(Authorization)回答「你能做什么」
  • 认证是授权的前提,但两者在实现上应该解耦
  • AI 项目里认证通常是用户身份、API Key、服务账号;授权通常是模型配额、功能开关、租户隔离
  • 把认证和授权混在一起写,后期加权限模型或换认证方式时会牵一发动全身
  • Hono 的中间件体系可以把认证和授权拆成独立层,职责清晰

1. 两个概念

认证(Authentication,简称 AuthN)是验证身份的过程:

  • 用户输入账号密码,系统确认是本人
  • 请求带上 API Key,系统识别是哪个开发者
  • 服务之间用 mTLS,双方确认身份

授权(Authorization,简称 AuthZ)是判断已认证的主体能做什么:

  • 普通用户不能访问管理接口
  • 免费用户每月只能用 10 次 GPT-4
  • 租户 A 不能读取租户 B 的数据

两者关系:先认证,后授权。认证失败直接拒绝,认证通过再检查权限。

2. 实现层面的区别

2.1 认证的实现

认证的核心是「证明你是谁」,常见方式:

方式场景凭据
用户名+密码Web 登录密码
JWT无状态 APIToken
Session有状态 WebCookie
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))
})

总结

认证是「你是谁」,授权是「你能做什么」。两者在概念和实现上都应该解耦。

这一节涉及到的几个要点:

  1. 认证 vs 授权:认证验证身份,授权检查权限
  2. 解耦:认证和授权分层实现,互不耦合
  3. AI 项目特点:多维度授权、多租户隔离、服务账号
  4. Hono 实现:中间件分层,职责清晰
  5. 常见错误:状态码用错、授权和业务耦合、忽略租户隔离

认证和授权是系统安全的基石。分层实现、职责清晰,后期扩展才不会牵一发动全身。

下一篇看用户注册与登录——注册流程、登录流程、输入校验、错误处理。