Skip to content

认证、授权与会话

认证、授权和会话常被一起称为“登录”,但它们解决的是不同问题。混在一起实现,容易留下越权、过期状态不一致和难以审计的漏洞。

三个概念分别回答什么

概念要回答的问题典型输出
认证 Authentication请求者是谁?用户或服务身份
授权 Authorization该身份能执行什么操作?允许或拒绝
会话 Session身份如何在后续请求中延续?Cookie、令牌或服务端会话

用户已登录,不等于可以操作任何资源。每个读取、修改、删除操作都要基于当前身份、资源归属和角色规则重新判断。

在服务端建立可信身份

客户端传来的用户 ID、角色字段都不可信。服务端应从经过验证的会话或令牌中提取主体身份,再传给业务服务。

text
请求凭据
  → 验证签名 / 会话有效性
  → 得到 subject、角色、过期时间
  → 执行业务级权限判断

身份验证失败返回未认证;身份有效但无权操作目标资源时返回无权限。两种情况对前端提示、审计和安全排查都不同。

常见错误:信任客户端传来的用户 ID

把请求体或 URL 参数里的 userId 直接当身份用,是后端最危险的越权漏洞(IDOR):

js
// 任何人都能改成别人的 userId
app.get('/orders', (req, res) => {
  const userId = req.query.userId   // 客户端可控!
  return db.query('SELECT * FROM orders WHERE user_id = ?', [userId])
})

症状:用户 A 把 URL 里的 ?userId=1 改成 ?userId=2,就看到了用户 B 的全部订单。更严重的是攻击者写个脚本遍历 id,能把全库用户数据拖走。这种漏洞不会报错、不会触发监控,通常要等数据泄露才被发现。

为什么错req.query.userId 是客户端可以任意篡改的值,把它当身份等于让用户自己声明"我是谁"。身份必须来自服务端验证过的会话或令牌,绝不能来自请求参数。

正确做法:从验证后的凭据里取身份,再按归属判断权限:

js
app.get('/orders', (req, res) => {
  const actor = req.user  // 来自已验证的会话/令牌
  // 只能查自己的,管理员才能查指定用户
  const targetId = actor.role === 'admin' ? req.query.userId : actor.id
  return db.query('SELECT * FROM orders WHERE user_id = ?', [targetId])
})

记住:凡是客户端传来的身份字段,默认都是不可信的。身份只能从服务端验证过的凭据里提取。

授权规则靠近业务动作

“只能编辑自己创建的文章”“管理员可查看全部订单”属于业务规则。不要只在菜单隐藏按钮,也不要只靠路由层保护。

js
function canUpdateArticle(actor, article) {
  return actor.role === 'admin' || article.authorId === actor.id
}

规则复杂时,统一收敛为策略函数或权限服务,避免每个接口各自写一版条件判断而逐渐不一致。

会话设计需要考虑失效与撤销

  • 凭据必须有明确的有效期和刷新策略。
  • 登出、密码修改、账号禁用后,应能撤销旧会话。
  • 敏感操作可要求重新验证,而不是无限信任长会话。
  • 权限变更后,旧会话中的授权信息不能长期滞后。

访问控制的目标不是增加阻碍,而是让每一次敏感操作都能回答:谁,在什么条件下,对哪个资源做了什么。

为复用而记录,为理解而整理。