认证、授权与会话
认证、授权和会话常被一起称为“登录”,但它们解决的是不同问题。混在一起实现,容易留下越权、过期状态不一致和难以审计的漏洞。
三个概念分别回答什么
| 概念 | 要回答的问题 | 典型输出 |
|---|---|---|
| 认证 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
}规则复杂时,统一收敛为策略函数或权限服务,避免每个接口各自写一版条件判断而逐渐不一致。
会话设计需要考虑失效与撤销
- 凭据必须有明确的有效期和刷新策略。
- 登出、密码修改、账号禁用后,应能撤销旧会话。
- 敏感操作可要求重新验证,而不是无限信任长会话。
- 权限变更后,旧会话中的授权信息不能长期滞后。
访问控制的目标不是增加阻碍,而是让每一次敏感操作都能回答:谁,在什么条件下,对哪个资源做了什么。