09.07 认证与授权:从登录主体到会话消息权限
返回第九卷 · 需求边界:09.01 · 数据访问:09.05 · 浏览器前置:09.06
09.07 认证与授权:从登录主体到会话消息权限
DeepTutor 初稿经技术与教学审阅后的静态课程。账号、Cookie、Token、会话和错误均为虚构纸上设计;本次没有运行 Go、登录服务、数据库、浏览器或站点。本章不声称核对过 OpenIM 的认证/授权实现,也不把示例策略写成生产配置。
一、已登录的 u-c 为什么仍不能读 c-a
09.06 解释了浏览器怎样携 Cookie、服务端怎样找到 Session。现在假设三位虚构用户都已成功登录:u-a 是会话 c-a 普通成员,u-b 是该会话管理员,u-c 不在 c-a。u-c 知道 GET /v1/conversations/c-a/messages 的路径和 m-a 的 ID,仍不能读这段历史。登录解决的是认证(Authentication):这次请求是谁;会话和消息操作还要做授权(Authorization):这个主体此时能对这个对象执行什么动作。
本章选一个简单、明确的教学政策:当前有效成员可读当前会话历史并发消息;该会话管理员可管理成员;非成员默认拒绝。这不是所有 IM 产品的退群历史政策。若产品允许退群者读取过去某段历史,应先给 06.03 的成员资格历史和时间范围建立新规则,不能靠“曾经登录过”或“知道会话 ID”推导权限。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 凭据、登录、Session/Token、主体 | 对象级权限、退群/注销/多设备撤销 | subject×action×resource 权限矩阵与旧连接拒绝时间线 |
二、口令是登录凭据,不能当长期会话状态
用户名和密码用于证明登录者,不是每发一条消息都在请求体重传一次密码。服务端不能保存明文密码,也不应只做一次快速 SHA-256:若密码库泄露,攻击者可高速离线猜测。密码应使用适合口令的慢且可调成本的哈希方案,如 OWASP 当前推荐的 Argon2id;每个账号需要独立随机盐,并记录算法、参数、盐和校验值,未来可按策略升级成本。盐通常与哈希结果一起保存;它的作用是使相同密码的哈希不必相同,不是第二个需要保密的密码。
Go 的 golang.org/x/crypto/argon2 提供 Argon2id 的底层派生函数,但调用一个 IDKey 并不能自动完成安全的账号系统:随机盐、参数选择、编码格式、比较方式、迁移旧哈希、限速和凭据泄露处理仍需成熟实现与审查。本章只讲设计边界,不提供可直接复制进生产的密码实现或参数常量。
登录失败对外可用不泄露账号是否存在的通用反馈;内部仍分类记录失败原因和受控关联信息,限制连续尝试与异常流量,避免把口令、完整 Token 或验证码写入日志。密码正确也只建立了一个已认证主体,不自动授予任何会话的读取、发言或管理员权限。对修改密码、增加权限等高风险操作,服务端可能要求重新认证,并重新评估已有会话状态。
三、Session 与 Bearer Token:都是后续请求的凭据
浏览器 Cookie 常携一个不可预测且无业务含义的 Session ID;服务端把它映射到 u-a、设备/客户端、创建与过期时间、撤销状态。Session ID 应由安全随机源产生,不能直接用 user_id 或可预测计数拼出。登录成功或权限级别改变时应更新会话标识,防止把登录前或低权限阶段的既有 ID 沿用为高权限会话。服务端负责检查过期与注销,不能只靠前端页面隐藏“退出”按钮。
另一类客户端可能在 Authorization: Bearer <token> 中主动附带令牌。Bearer 的核心风险是“谁拿到有效令牌,谁就可能以其权限使用它”;因此传输、保存、日志、有效期和撤销都需要控制。Token 可能是不透明随机标识,也可能是有签名的 JWT。JWT 签名验证能证明令牌内容在相应密钥与校验规则下未被篡改,不能证明 u-a 现在仍属于 c-a,也不能让已签发令牌在用户退群/注销时自动失效。需要短有效期、服务端状态查询、版本或撤销记录等符合产品目标的策略。
| 载体/表示 | 典型获取主体的方式 | 当前权限与撤销要看什么 |
|---|---|---|
| 浏览器 Cookie 中的 opaque Session ID | 服务端查 Session 状态 | Session 有效;会话成员关系另外核对 |
| 主动附带的 opaque Bearer Token | 服务端查 Token 状态 | 过期/撤销状态;具体资源权限另外核对 |
| 已验证签名的 JWT | 验证签名、发行者/受众/时间等条件后取声明 | 退群、角色变更及即时撤销仍需额外机制 |
“Cookie”和“Token”也不是互斥分类:Cookie 可以装令牌,Header 可以装 opaque ID。浏览器自动附带 Cookie 才带来 09.06 讨论的 CSRF 风险;脚本主动设置 Bearer 头的路径不同,但脚本可读的存储又有 XSS/泄露风险。先定义客户端形态和威胁,再选择会话机制,不能说“JWT 天然安全”或“换成 Token 就无需授权”。
四、角色只是条件之一,必须对对象做权限决定
把权限写成一个可审阅的判断:Can(subject, action, resource, currentState)。subject 来自认证结果;action 是读历史、发消息、邀请成员等;resource 是具体 c-a 或 m-a;currentState 包含成员状态和会话内角色。未命中明确允许规则时默认拒绝。权限检查要发生在每条实际请求/操作上,而不是只在前端菜单上隐藏按钮。
| 主体与当前状态 | 读 c-a 历史 | 发到 c-a | 邀请/移除 c-a 成员 | 读无关会话 |
|---|---|---|---|---|
u-a:c-a 普通成员 | 允许 | 允许 | 拒绝 | 按无关会话权限另判 |
u-b:c-a 管理员 | 允许 | 允许 | 按管理员规则允许 | 不因 c-a 管理员身份自动允许 |
u-c:已登录但非 c-a 成员 | 拒绝 | 拒绝 | 拒绝 | 按该会话权限另判 |
| 未认证请求 | 拒绝 | 拒绝 | 拒绝 | 拒绝 |
这张表只是起步政策。管理员是否能移除最后一位管理员、能否查看退群前消息、被禁言成员能否读历史,都必须有单独规则和竞态测试。尤其要区分全局平台角色与某个会话内角色;u-b 是 c-a 管理员,不能因此自动读取所有人的私聊。
攻击者可把请求中的 conversation_id=c-a 改成 c-b,或把 message_id=m-a 改成别的消息 ID。ID 是定位对象的输入,不是授权凭证;即使 ID 很长或随机,也要检查登录主体对该对象及其会话是否有权限。OWASP 把这类漏检称为对象级授权失效(BOLA)。只比较 subjectID == senderID 也不总够:历史消息的可见性可能由会话成员资格、发送时/当前时间范围和产品规则共同决定。
五、发送和读历史从哪里得到“可信发送者”
读历史时,handler 从服务端已验证的 Session/Token 获取 subject=u-a,再解析客户端给的 conversation_id、游标和页大小;应用按 u-a 对 c-a 的可见范围查询。发消息时,客户端可提供会话和正文,却不能凭请求体的 sender_id=u-b 让服务端代 B 发信;消息记录中的发送者应由受信主体确定。数据访问层还需在正确的事务/快照边界维护成员状态与写入一致性,见 06.07/09.05。
可信 Session/Token → subject=u-a
客户端输入 → conversation_id=c-a, body="好", cursor=2
服务端授权 → Can(u-a, send/read, c-a, 当前成员状态)
受控数据访问 → 只写/读属于允许范围的消息“没有历史行”和“没有权限看历史”在服务端是不同原因。对外可按 09.02 的防枚举合同选择拒绝或统一未找到,但内部必须保留准确分类;数据库断连、超时不能伪装成无权限。HTTP 与 WebSocket 也应执行同一业务权限:握手确认 u-a 的身份后,旧连接上的每条发送/订阅仍须看目标会话和当前资格。客户端传来的 user_id、群角色或消息归属均不代替服务端核对。
06.07 的退群竞态仍在:T1 查到 A 是成员,T2 让 A 退出,T1 再写消息。授权函数存在不等于并发顺序正确;产品需规定“发送与退群谁先被裁决”,再用共同冲突边界、事务与重试实现。若只在连接建立时查一次成员,就更不能保证长连接期间权限未变。
六、注销、退群与两台设备不是一种撤销
假设 A 同时在浏览器与手机登录,各有独立 Session/Token。A 点击“退出当前设备”应使浏览器凭据无效,却未必撤销手机;点击“退出所有设备”则需按合同撤销所有对应凭据。Cookie 过期是浏览器侧动作,服务端仍须拒绝被窃取或重放的旧凭据。对有签名的长期 Token,若业务要求立即全设备撤销,单靠等待 exp 不够,需要服务端状态/版本/撤销机制。
退群是资源授权变化,不等于账号登出。A 退 c-a 后仍是 u-a,可能仍可看其他会话;但本章政策下不得再读/发 c-a。旧 WebSocket 如果仍连着,服务端应在后续订阅/发送时重新评估资格,或按有明确延迟上界的失效机制关闭相关订阅。若系统有权限缓存,必须说明失效来源、最长陈旧时间和竞态结果,不能只写“退群后立即无权”却保留可无限期使用的旧缓存。
| 事件 | 身份仍有效吗 | c-a 权限怎样变 | 需核对的旧通道 |
|---|---|---|---|
| 浏览器当前设备注销 | 该设备 Session 应失效 | 用户关系可不变 | 旧 Cookie、该设备 HTTP/WS |
| 全设备注销 | 该用户已签发的目标凭据按合同失效 | 用户关系可不变 | 手机、电脑与旧 Token |
A 退出 c-a | A 可继续登录 | 本章政策下读/发 c-a 被撤销 | 旧订阅、历史接口、发送接口 |
B 被取消 c-a 管理员 | B 仍可登录且可能仍是成员 | 管理动作被撤销,普通成员动作另判 | 旧角色声明/缓存与管理入口 |
JWT 中的角色声明可能比服务端成员状态旧;如果用它独自决定长期权限,就会延迟撤销。需要把“令牌可用多久”“每次是否查权威成员状态”“缓存多久同步”写成显式合同。多设备消息送达与多设备登录会话是两套状态,不能因为撤销一台设备 Token 就把消息已读状态改掉。
七、错误与日志也要守住身份边界
未提供有效凭据,服务端无法建立主体;已认证但对 c-a 无权,是不同条件。HTTP 可按既有接口合同使用 401、403,或者为降低对象枚举风险对某些资源统一返回 404;对外隐藏细节不等于内部失去分类。数据库故障、依赖超时或写入结果未知也不能为了“看起来安全”一律归入 403。错误体不能回显密码、Session ID、Bearer Token 或完整消息正文。
日志最少记录受控的请求关联、动作、固定路由、已核对主体的受限标识、结果类别和必要错误码;账号登录、会话签发、撤销、权限拒绝和敏感管理动作应可审计,但不要把原始凭据写入日志。10.02/10.09 的验证分层可用于这组静态案例:
| 负例 | 必须写出的预期 | 不能由纸上设计冒充 |
|---|---|---|
u-c 把 URL 改为 /conversations/c-a/messages | 仍拒绝读 c-a | 实际 HTTP 测试已通过 |
A 请求体伪造 sender_id=u-b | 发送者仍由受信 u-a 决定或请求被拒绝 | 所有下游都正确传播身份 |
| A 退群后复用旧 WebSocket | 后续 c-a 发送/订阅被拒绝 | 真实撤销传播延迟已经测量 |
| 浏览器注销后重放旧 Cookie | 服务端不再认可该 Session | 其他设备也自动退出 |
本章没有运行登录服务或鉴权测试。以后学习者在隔离环境验证时,应分别保存代码版本、政策版本、正/负例输入、返回结果和服务端分类证据;生产权限的真实保证还需要审阅每条入口及实际撤销路径。
八、交付权限矩阵与分层练习
交付一条“登录→Session→读 c-a→发 m-a→退群→旧连接再试”的纸上时间线、一张主体/动作/对象权限矩阵,以及一份“当前设备注销、全设备注销、退群、角色变更”的撤销表。为每项写明受信身份来源、权限权威数据、过期/缓存时间界限和失败时的可观察证据。静态课程只供学习与个人实践设计,不声称已有实际账号系统。
分层练习:先答,再展开反馈
1. 认证与授权分别回答什么?
认证回答请求主体是谁;授权回答这个主体能否对具体对象做具体动作。
2. 已登录的 u-c 能读 c-a 吗?
按本章当前成员政策不能;u-c 不在 c-a。
3. 为什么不能只存快速 SHA-256(password)?
它便于高速度离线猜测;口令需要慢且可调成本的专用哈希方案。
4. 每用户独立盐主要解决什么?
相同密码不必产生相同哈希,降低预计算攻击收益。
5. 调一次 Argon2id 底层函数就得到完整安全登录系统吗?
不能。还要处理盐、参数、编码、比较、迁移、限速和会话生命周期。
6. 服务器 Session ID 能直接等于 user_id 吗?
不应。它应不可预测且无业务含义,主体信息由服务端映射。
7. 登录或权限升级为什么要更新 Session ID?
避免此前低权限或被固定的会话标识继续作为高权限凭据。
8. Bearer Token 的核心风险是什么?
持有有效令牌的人可能使用其权限,故必须保护传输、保存与撤销。
9. JWT 签名通过就证明 A 仍属于 c-a 吗?
不能。成员关系可能已变化,需按当前权限合同核对。
10. Cookie 与 Token 一定互斥吗?
不一定。Cookie 是浏览器传输载体,里面也可以承载某种令牌。
11. u-b 是 c-a 管理员,就能读所有私聊吗?
不能。角色作用于其定义的资源范围。
12. 知道随机且很长的 message_id 能跳过授权吗?
不能。ID 定位对象,不是允许读取该对象的证据。
13. 把 conversation_id 从 c-a 改成 c-b,服务端该做什么?
以已认证主体对 c-b 重新做对象级权限判断。
14. 客户端正文写 sender_id=u-b 可让 A 代 B 发消息吗?
不能。发送者要由受信认证上下文决定。
15. 握手时通过成员校验,后续 WebSocket 消息可免查吗?
不能。成员和会话状态可能变化,后续动作仍须授权。
16. A 退群等于 A 账号登出吗?
不是。账号身份可仍有效,c-a 资源权限按政策撤销。
17. 当前设备注销会自动撤销手机会话吗?
不一定。需要区分当前设备和全设备的撤销合同。
18. 长期 JWT 的 exp 能保证“立刻”撤销吗?
不能。即时撤销还需服务端状态/版本/撤销机制。
19. 权限缓存为何要记录最长陈旧时间?
退群/角色变更后旧权限可能仍被缓存使用,合同需明确延迟边界。
20. 无凭据与已登录但无权读 c-a 必须在内部区分吗?
必须。对外状态可按防枚举合同选择,内部仍需正确分类。
21. 权限拒绝日志可以存完整 Token 吗?
不应。记录受控关联和错误类别即可,原始凭据不得进普通日志。
22. 撤销登录 Token 会自动把 m-a 标为未读吗?
不会。登录会话和消息送达/已读是不同业务状态。
来源与下一步
- OWASP:认证、口令存储、会话管理与授权:凭据、Session、默认拒绝和逐请求权限。
- OWASP:对象级授权失效、JWT 指南与Bearer Token 标准:对象 ID、令牌持有与撤销边界。
- Go
x/crypto/argon2:Go 的 Argon2id 底层 API,仅作算法入口,不替代完整口令系统设计。
按学习路线,下一章09.08 输入与输出防护把已学 SQL、浏览器和认证前置连到注入、输出编码、文件与 URL、大小限制和敏感日志;更完整的多设备消息确认留到 S5。离开本章前,应能对任一 IM 请求写出已认证主体、具体动作、具体资源、当前权限权威来源和撤销边界。