在上一篇的安全挑战地图中,我们把身份冒用、会话劫持和越权放在同一条请求路径上观察。它们发生的位置不同,却可能带来同一个结果:一份文档被不该修改它的人改掉了。
假设 Guang 是项目 A 的编辑者。他登录协作文档系统后,提交了这样一个请求:
PATCH /api/documents/doc-42
如果服务器只检查“有没有登录”,Greg 就可能用自己的账号修改无权访问的 doc-42。如果服务器只检查“是不是编辑者”,Guang 又可能修改项目 B 的文档。即使这两项都检查了,他刚被移出项目时,一张尚未到期的旧令牌仍可能被接受。
因此,这次修改需要经过几次不同的判断:找到账号,验证使用账号的依据,让后续请求延续登录状态,再检查对 doc-42 的具体权限。依据发生变化后,系统还要让旧的许可失效。本文沿着这条路径解释各项机制;协议流程用于说明原理,业务规则则以这个假设的文档系统为准。
请求中的 Guang,究竟是哪一个账号
找到账号、证明身份和允许操作是三件事
Guang 在登录框中输入用户名,只是告诉应用要查找哪个账号。任何人都能输入同样的名字,所以应用还要验证密码或其他证明。通过验证后,它才有依据把请求关联到 Guang。
访问控制把据以分配权限、记录操作的实体称为主体(Principal)。这里的主体是 Guang 的账号,也可以是服务、后台任务或设备;公开资源还可以允许匿名访问。
这几个步骤对应三个概念:
| 概念 | 在这次请求中解决的问题 |
|---|---|
| 身份识别(Identification) | 找到 Guang 的账号 |
| 身份认证(Authentication) | 验证请求方使用这个账号的依据 |
| 授权判断(Authorization) | 判断该账号能否编辑 doc-42 |
本文用“认证”和“授权判断”区分后两项,避免把它们都叫作“鉴权”。授权还包括授予权限的过程;处理请求时,我们关心已建立的权限如何被检查和执行。OWASP Authorization 指南
先看一条使用服务端会话和 Cookie 的完整路径。登录验证发生在前半段,具体资源授权发生在后半段。后文会逐步展开其中的判断依据。
Guang 换了邮箱,已有的文档关系怎样保留
如果系统把邮箱直接当作所有业务关系的永久标识,Guang 更换邮箱时,文档归属和成员关系就很难保持稳定。应用通常用内部 ID 关联这些记录,把用户名、邮箱和手机号作为可以变更的登录标识或联系方式。
验证邮箱验证码,说明请求方能接收或取得相应消息;验证密码,说明请求方知道绑定在账号上的秘密。两者都不能直接证明现实中的姓名或机构。业务若需要这些事实,还要完成身份核验(Identity Proofing)。OWASP Authentication 指南
换成企业账号登录,这个问题仍然存在。用于传递外部认证结果的 OpenID Connect(OIDC)使用 iss 标识签发者,使用 sub 标识该签发者下的主体。验证结果后,应用用这对值映射本地账号。不同签发者的 sub 可能重复;采用成对主体标识时,同一个人在不同客户端看到的 sub 也可能不同。OpenID Connect Core §5.7、§8
所以,企业账号与本地账号“邮箱相同”,还不足以无条件合并账号。关联身份会改变账号控制权,应用必须先取得可靠的绑定依据。身份识别解决的是“找到谁”,接下来才是“凭什么相信请求方可以使用这个身份”。
身份认证:有哪些证明方式,如何比较
Guang 可以输入密码、提交验证码,也可以让认证器生成签名。先按应用收到并验证什么,把这些直接认证方式分成三种思路:验证长期秘密、验证临时秘密或动态验证码、验证公钥签名。下面分别推演它们怎样工作,再比较优势、代价与适用场景。
还有一个独立选择:由文档应用验证 Guang 的凭据,还是接受企业身份提供方的认证结果。后者属于联邦认证,即系统之间基于约定的信任关系传递认证结果。企业身份提供方内部仍可以使用密码或公钥凭据;后面的企业登录章节会比较这项责任如何分配。登录后怎样延续身份,则放在会话章节讨论。
密码验证了秘密,也把泄露风险带进了系统
Guang 输入密码,应用需要判断它是否与账号绑定的密码匹配。服务端保存专门的密码哈希结果和参数,由密码验证库完成比较,不需要保留能还原的明文密码。
为什么不能直接用一个快速哈希函数?因为数据库泄露后,攻击者可以离线枚举候选密码,登录接口的限流已经不起作用。密码哈希要让每次猜测付出足够的成本。
随机盐(Salt)是为每份密码哈希单独生成、参与计算的随机值。它让相同密码也不会简单对应相同结果,减少跨账号复用预计算成果的机会。盐可以与哈希一起保存,无需保密。
补充说明:密码哈希算法。 Argon2id 通过计算与内存开销增加密码猜测成本。具体参数需要结合部署环境和当前建议选择。OWASP Password Storage
这解决了存储泄露后的一部分风险。在线撞库仍需要限制尝试次数,密码传输仍需要保护,钓鱼页面仍可能直接取得 Guang 输入的密码。后两类问题不会因为数据库保存了哈希就消失。
验证码用过即失效,是否就足够安全
为了减少长期秘密的重复使用,系统可以接受一次性密码(One-Time Password,OTP)。“一次性”约束成功使用的次数,有效期则是另一项约束。
短信或邮件验证码由服务器生成并发送。服务器负责检查有效期,并在成功使用后使其失效。基于时间的一次性密码(Time-Based One-Time Password,TOTP)则由验证器和服务器根据共享密钥、当前时间段分别计算,所以验证器能离线生成验证码。
服务器可以检查相邻时间段,以容忍时钟偏差。但已成功使用的 TOTP 不能在它仍可被接受的时间窗口内再次使用;倒计时本身不会完成这项控制。RFC 6238 §4、§5.2
补充说明:计数器与有效期。 HOTP(HMAC-Based One-Time Password)根据共享密钥和递增计数器生成一次性密码,成功验证后推进服务端计数器。这里的 HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)使用共享密钥计算并校验认证码。没有额外过期策略时,未使用的 HOTP 不会仅因时间流逝失效。RFC 4226 §5.2、§7.2
Magic Link 则把临时秘密放进登录链接。Guang 点击链接,相当于向应用提交这个秘密。因此,链接本身就是凭据,应用需要控制用途、有效期、单次使用和日志暴露。
这些方式可以缩短证明的使用范围,却仍可能被实时转发。攻击者诱导 Guang 输入验证码后,立刻把它提交给真实站点,仍可能完成认证。应用还要保护凭据绑定和传递渠道,并限制重复尝试。OWASP Multifactor Authentication
Passkey 怎样让证明与站点绑定
密码和 TOTP 都依赖共享秘密。Passkey(通行密钥)改用公钥凭据:认证器管理私钥,站点保存注册时绑定的公钥。凭据可以按产品策略在设备间同步,也可以绑定于特定设备。FIDO Alliance:Passkeys
在 Web 中,浏览器通过 WebAuthn(Web Authentication,Web 认证)接口协调站点与认证器。使用认证结果的站点称为依赖方(Relying Party,RP);依赖方标识(Relying Party Identifier,RP ID)是限定凭据范围的域名字符串。WebAuthn:术语
Guang 登录时,服务端发出新鲜的随机挑战。认证器用私钥生成签名,经浏览器返回响应。服务端用已绑定的公钥验证签名,还要检查挑战、来源、RP ID、用户在场标志,以及策略要求的用户验证结果。
本地 PIN(Personal Identification Number,个人识别码)、指纹或面容可以用于解锁这次私钥操作。网站收到的是协议结果,不会因此取得指纹或面容数据。浏览器对站点上下文的约束,也使仿冒页面不能仅凭相同外观,就替真实站点取得相同的认证证明。WebAuthn:认证验证与本地生物识别
下图把注册与认证分开。认证器返回签名和认证器数据,浏览器附上自己构造的客户端数据,再提交给站点。客户端数据包含挑战和来源;签名绑定认证器数据与客户端数据的摘要,站点还需核对它们是否符合本次请求。WebAuthn:认证响应
这份证明最终依赖注册时的绑定。如果攻击者能通过恢复入口,把自己的 Passkey 加到 Guang 的账号上,后续签名也会验证通过。公钥认证解决了证明怎样产生和校验的问题,账号绑定与恢复仍需独立保护。
MFA 与增强认证:这次操作需要多强的证明
前面比较的是证明怎样生成和验证。接下来还要决定:一次登录需要组合哪些证明,它们能支持哪些操作?
多因素认证(Multi-Factor Authentication,MFA)组合不同类别的因素,例如知道的秘密、持有的认证器,以及指纹等自身特征。密码加 TOTP 可以组合知识与持有因素,两个密码框则仍属于同一类。“无密码”也不自动等于满足多因素要求;Passkey 的保证取决于凭据类型、用户验证方式和系统要求。
Guang 普通编辑时可以延续已完成的认证。但他要绑定新的认证器时,仅凭普通登录状态就放行,会让窃取会话的人也能绑定自己的凭据。此时可以要求增强认证(step-up authentication):取得满足这次操作要求的证明,例如更近期的认证或更强的认证方式。应用据此检查认证时间与已满足的条件。RFC 9470:增强认证要求
MFA 减少了单一因素泄露的影响,代价是额外的绑定、使用和恢复流程。增强认证把额外验证集中在敏感操作,减少日常编辑中的打断,但需要明确触发条件和结果复用时间。两者都可以与前面的登录方式组合。
如果 Guang 丢失了认证器,系统还需要通过备用凭据、恢复码或人工核验重新确认账号控制权。恢复入口会重新建立凭据绑定,也必须有相应的保护;否则主入口要求再强的证明,攻击者仍可能绕到恢复入口接管账号。OWASP MFA:因素变更与恢复
认证方式怎样比较,Guang 该选哪一种
现在可以把前面的机制放到同一组问题下比较:它减少了哪种风险,新增了什么依赖,用户能否顺利使用和恢复?下表的场景是基于这些机制特点作出的选型判断。
| 方式与适用场景 | 优势与主要代价 |
|---|---|
| 密码:需要覆盖广泛客户端,或逐步升级已有账号体系 | 兼容性好,不依赖消息投递。代价是需要处理密码存储、撞库、钓鱼和找回;用户还要管理长期秘密。 |
| 短信验证码:风险较低、需要兼容普通手机的登录入口 | 无需安装认证器。代价是投递费用与延迟、手机号被接管及钓鱼风险;不适合作为高价值账号的主要保护。 |
| 邮件验证码:接受邮箱控制权作为依据的低风险、低频登录 | 无需额外认证设备,也无需记忆站点密码。代价是依赖邮箱安全与投递,仍可被钓鱼转发。 |
| TOTP:为已有密码登录增加一个持有因素 | 验证器能离线生成代码,不依赖逐次短信投递。代价是共享密钥的保管、设备迁移和恢复;短有效期仍不能抵抗实时钓鱼。 |
| Magic Link:接受邮箱作为信任基础,希望减少输入的低频登录 | 点击即可提交临时证明。代价是邮箱依赖、链接泄露和跨设备流程衔接;“免输入”不会提高邮箱本身的可信程度。 |
| Passkey:需要防钓鱼登录,且目标设备支持相应认证器 | 通过站点绑定的公钥证明减少可被转交的登录秘密,站点不保存用户私钥。代价是需要设计设备兼容、凭据同步或迁移,以及恢复路径。 |
密码、短信、邮件和 TOTP 的风险依据见 OWASP:认证因素的优缺点;Passkey 的凭据与防钓鱼特性见 FIDO Alliance:Passkeys。
如果文档系统保存商业敏感资料,且 Guang 的设备支持认证器,可以优先采用 Passkey。已有密码用户可以逐步增加 TOTP,同时接受它仍可能被实时钓鱼的边界。邮件验证码或 Magic Link 则把更多信任放在邮箱上。选定主入口后,还要让备用凭据与恢复流程满足同一套账号保护要求。
登录完成后,下一次修改请求凭什么被接受
会话怎样延续一次认证
Guang 连续编辑文档时,不应该每次都重新输入密码。应用可以把认证结果延续为会话(Session)。
在前面的时序图中,服务端创建会话记录,保存用户 ID、认证时间、到期时间和撤销状态,再把随机会话标识交给浏览器。后续请求携带这个标识,服务端查找记录,判断会话是否有效。
后续请求用会话凭据代替完整的登录证明,因此拿到它的人可能冒用会话。这个值需要难以预测,不能直接用可公开的用户 ID 代替。
一个典型问题是会话固定(Session Fixation)。攻击者先知道或设定未认证的会话标识,再诱导 Guang 用它登录。如果应用沿用同一标识,攻击者也能使用升级后的会话。因此,登录及权限提升时需要更新会话标识,使旧标识失效。OWASP Session Management
服务端状态便于查询与撤销,但多个实例必须共享或同步理解它。中央记录已经删除、某个实例却仍缓存“有效”,会话就没有在所有入口同时失效。
Cookie 负责携带什么,令牌又是什么
浏览器如何把会话标识交回来?一种方式是 Cookie:浏览器保存数据,并按规则随请求发送。下面是 HTTPS 响应中的设置示意,占位值需由服务端生成:
Set-Cookie: __Host-session=<随机会话标识>; Path=/; Secure; HttpOnly; SameSite=Lax
Secure 约束安全传输,HttpOnly 限制页面脚本直接读取,SameSite 参与跨站发送判断。__Host- 前缀要求安全来源、Path=/,且不能设置 Domain。这些规则保护凭据的传输和使用方式,不决定服务端会话何时到期。MDN:Set-Cookie
这里的“同站”与“同源”也不同。同源比较协议、主机和端口;现代 SameSite 判断使用协议与可注册域等站点信息。两个子域可能同站但不同源,SameSite 因而不能充当精确的可信来源列表。
令牌(Token)是系统接受为某种证明或许可的数据。会话标识就是一种令牌。这样,三个概念的职责就清楚了:会话延续状态,令牌承载证明,Cookie 负责保存和传递数据。
令牌也可以由 JavaScript 放进 Authorization 请求头。选择存储和发送位置,会带来不同的风险:
| 持有与发送方式 | 风险与限制 |
|---|---|
HttpOnly Cookie:浏览器按规则发送 |
脚本不能直接读值,但 XSS 仍可能借会话操作;需要处理 CSRF。 |
| JavaScript 内存:代码构造请求头 | 页面重载后需要重新取得凭据;恶意脚本可能干预调用。 |
localStorage 等存储:代码读取凭据并构造请求头 |
同源恶意脚本可能读走凭据,持久化会延长暴露机会。 |
存入 localStorage 不会让浏览器自动发送凭据;放进 Cookie 也不能单独证明修改请求符合 Guang 的意愿。这些浏览器风险已在上一篇中区分,后续文章再展开防护。OWASP HTML5 存储安全指南
如果把身份信息直接写进令牌
随机会话标识指向服务端记录,客户端不需要理解它的内部含义,这类值常被称为不透明令牌。另一种思路是让令牌直接携带可验证的声明,JSON Web Token(JWT)就是常见格式。JWT 可以放进 Cookie,不透明令牌也可以放进请求头;格式和传输方式可以分别选择。
常见的签名型 JWT 使用 JWS(JSON Web Signature,JSON Web 签名),以数字签名或消息认证码保护完整性。紧凑格式的三个部分分别经过 Base64URL 编码,再用点号连接:RFC 7515 §3.1
编码后的头部.编码后的声明.编码后的签名或消息认证码
头部描述算法等信息,声明可以携带主体、签发者、接收者和有效期。接收方验证这些信息后,可以在本地完成部分判断,减少逐次查询中央状态的需要。
但“能解码”还不是“可信”。接收方必须用预先配置的算法和可信密钥验证,并检查 iss、aud 是否符合预期。必需声明、exp、nbf 和用途,则按令牌类型及协议检查。同样是 JWT,也不能把一个入口接受的凭据直接交给另一个入口使用。RFC 8725 §3
签名型 JWT 的头部和声明可以被读取。JWE(JSON Web Encryption,JSON Web 加密)通过加密保护机密性,并提供完整性校验;JWT 也可以采用这种封装。判断内容是否保密,要看具体封装,不能只看“JWT”这个名称。RFC 7516 §1、RFC 7519 §3
下面把两种验证路径放在同一张图中。引用型凭据需要查询权威状态;自包含声明可以在本地校验。两者可以混合使用,JWT 也能配合在线查询或撤销记录。
对 Guang 来说,差别最终落在一个问题上:令牌里保存的是哪一刻的事实?角色写进 JWT 后,成员关系变化不会改写已经发出的字符串。本地验证节省了查询,也要求系统明确如何处理过时的声明。
会话查询与自包含令牌校验,怎样取舍
这两条路径比较的是后续请求怎样取得可信身份依据。Guang 最初使用密码还是 Passkey,不决定这里必须选哪一条。
| 验证路径与适用场景 | 优势与主要代价 |
|---|---|
| 查询服务端会话:自有 Web 应用,需要集中管理登录状态和撤销 | 便于主动更新和撤销状态,客户端只持有随机标识。代价是请求依赖状态存储;多实例共享、本地缓存和存储故障都需要处理。 |
| 本地校验自包含令牌:多个接收方需要验证签发结果,并希望减少逐次中央查询 | 接收方可本地验证签名与声明。代价是密钥分发、令牌用途校验和过时声明处理;需要及时撤权时,仍可能引入在线状态检查。 |
对本文的编辑操作,一种自然的组合是用会话或 JWT 确定 Guang 的身份,再查询当前成员关系决定能否写入。这样保留了各自的验证便利,也把权限新鲜度留在业务判断中。后面的服务端架构和撤权时间线,会进一步检验这个组合。
令牌不能篡改,为什么仍然可能被冒用
攻击者不必把 Guang 的名字改进令牌。如果能复制他的有效令牌,也可能直接使用。
这就是 Bearer 的使用条件:持有令牌即可按其许可使用,不要求额外证明掌握某把密码学密钥。随机字符串和 JWT 都可以是 Bearer 令牌;签名防止篡改,却不会阻止复制。RFC 6750 §1.2
发送者约束令牌进一步把令牌与密钥绑定。接收方除验证令牌,还要验证调用方掌握相应私钥,例如检查绑定的客户端证书及持钥证明。只偷走字符串就不再一定能用,但私钥也被控制、绑定校验缺失或重放检查失效时,保护仍可能落空。
补充说明:应用层持钥证明。 DPoP(Demonstrating Proof of Possession,持有证明)让客户端用与令牌绑定的私钥生成请求证明。它绑定方法、目标地址等信息;地址不含查询和片段,证明也不自动覆盖请求正文。因此,订单金额或消息内容的完整性仍需其他机制保护。RFC 9449 §4、§11
到这里,应用已经有办法判断请求带来的身份依据是否可信。接下来要解决的,是这份依据是否允许修改眼前的文档。
授权判断:有哪些规则模型,如何选择
认证方式回答请求方凭什么使用 Guang 的账号;授权模型回答编辑许可从哪些事实推导。这里先比较直接许可、角色、属性和关系四种规则表达,再讨论临时许可怎样由凭证携带。判断部署在应用内还是统一授权服务、权限保存在数据库还是缓存中,是后面要处理的执行与状态问题。
先把一次编辑的规则写清楚
假设应用已经验证会话,得到 Guang 的账号。此时直接执行修改,仍然可能越权:Guang 也许属于另一个项目,或者在当前项目只有阅读权限。
一次授权判断需要把四类信息连起来:主体、资源、操作和上下文。这里分别是 Guang、doc-42、编辑,以及组织、项目成员关系和文档状态。请求中的文档 ID 只负责定位资源,不能替代这些事实。
给这个示例定一条规则:只有项目内的活跃编辑者或所有者,才能修改草稿。下面的代码可在 Node.js 24 中运行。它假设主体、文档和成员关系都已从服务端可信记录中加载,不负责认证或读取数据库。
function canEditDocument(subject, document, membership) {
if (!subject || subject.status !== "active") return false;
if (!document || document.state !== "draft") return false;
return Boolean(
membership &&
membership.status === "active" &&
membership.userId === subject.id &&
membership.tenantId === document.tenantId &&
membership.projectId === document.projectId &&
["owner", "editor"].includes(membership.role)
);
}
const guang = { id: "u-guang", status: "active" };
const document = {
id: "doc-42", tenantId: "org-a", projectId: "project-a", state: "draft"
};
const membership = {
userId: "u-guang", tenantId: "org-a", projectId: "project-a",
role: "editor", status: "active"
};
console.log(canEditDocument(guang, document, membership)); // true
console.log(canEditDocument(guang, document, {
...membership, projectId: "project-b"
})); // false:其他项目的角色
console.log(canEditDocument(guang, document, {
...membership, role: "reader"
})); // false:有成员身份,但没有编辑权限
console.log(canEditDocument(guang, document, {
...membership, status: "removed"
})); // false:成员关系已经失效
四次调用中,Guang 的账号没有变,结果却只有第一次为 true。另外三次分别改变了项目、角色和成员状态。这说明“是谁”相同,不代表“对这份文档能做什么”相同。
把函数放进真实接口,还要限制可修改字段。允许修改正文,不应顺带允许修改 tenantId 或所有者。若授权检查到写入之间可能发生状态变化,最终执行也要满足一致性要求;一次较早的检查不能保证之后一直有权操作。
ACL、RBAC、ABAC、ReBAC 分别怎样表达许可
上面的代码已经同时检查了角色、成员关系和文档属性。随着协作范围扩大,应用需要把这些事实组织成可维护的授权规则。
先增加一个业务前提:组织另行授予 Guang 分享 doc-42、委托工具读取这份文档的权限。编辑、分享和委托是不同操作,后两项不能从“编辑者”角色自动推出。
Guang 邀请 Greg 阅读 doc-42 时,应用可以为这份资源记录“Greg 可以读取”。这就是访问控制列表(Access Control List,ACL):直接记录主体对资源的许可。之后 Greg 发起读取,应用查找对应记录;移除该许可,就改变了下一次判断的依据。
如果一批项目成员都承担编辑职责,可以先把操作权限汇集成角色,再把角色授予成员。这是基于角色的访问控制(Role-Based Access Control,RBAC)。Guang 的“编辑者”角色限定在项目 A 内;把他改为“审阅者”,就改变他在这个范围内取得的权限集合。角色还可以组织成层级,并加入“提交人不能审批自己的申请”等职责分离约束。NIST:RBAC
但角色没有变化,文档也可能从草稿变成归档状态。如果许可还受组织、在职状态或密级约束,应用可以综合主体、资源、操作及环境属性求值。这是基于属性的访问控制(Attribute-Based Access Control,ABAC)。规则决定如何组合条件,可信数据源提供属性;用户自己提交的“所属组织”不能直接成为依据。NIST SP 800-162
再假设团队获得了项目 A 的编辑权,规则允许团队成员继承这项许可。Guang 属于该团队,doc-42 又属于项目 A,系统便能沿这条关系推导编辑权。这是基于关系的访问控制(Relationship-Based Access Control,ReBAC)。移除 Guang 的团队成员关系后,这条推导路径就不再成立;若还有其他授权路径,则需一并求值。Google Zanzibar 论文
下图把直接许可、角色、属性和关系画在同一份文档上,帮助观察判断分别从哪里开始。
授权模型的优劣,怎样对应业务需求
模型的选择要看权限主要随什么变化。Guang 直接分享一份文档、调整项目职责,或通过团队继承权限,带来的维护问题不同:
| 模型与适用场景 | 优势与主要代价 |
|---|---|
| ACL:逐份文档邀请协作者,直接授予例外许可 | 能直接查看谁对资源有什么许可。代价是资源和协作者增多后,授权记录、批量变更与继承关系更难维护。 |
| RBAC:项目内职责相对稳定,许多人共享编辑者、审阅者等权限集合 | 用角色复用权限,人员调整较直接。代价是大量资源例外和动态条件容易催生过多角色;角色仍须限定组织、项目等范围。 |
| ABAC:访问还受文档状态、组织、密级或环境条件约束 | 能组合条件,避免把每种条件组合都建成角色。代价是属性来源、新鲜度和策略复杂度;解释一次拒绝也需要还原当时的属性。 |
| ReBAC:团队、项目、文件夹之间存在共享与继承关系 | 沿关系推导许可,便于表达“团队成员可编辑团队项目”。代价是关系查询、继承规则与变更传播;复杂关系需要可解释的推导路径。 |
角色、属性和关系模型的取舍也可参考 OWASP 授权指南。具体成本仍取决于数据规模、规则和实现方式,不能仅凭模型名称判断性能。
在 doc-42 的示例中,可以用项目内角色表达编辑职责,用成员关系确定适用范围,再由文档状态限制写入。如果以后增加“通过团队继承项目权限”,再扩展关系推导。前面的 canEditDocument 已经组合了多类事实;它并不要求先选定一个模型,再把其他事实排除在外。
补充说明:谁能决定权限。 自主访问控制(Discretionary Access Control,DAC)允许资源所有者在规则范围内授予访问权。强制访问控制(Mandatory Access Control,MAC)由统一策略约束访问,常结合安全标签。它们区分授权由谁决定,与角色、属性等规则表达方式不是简单的替代关系。NIST:Discretionary Access Control
选定模型以后,仍要保证所有访问入口执行检查。详情页检查正确,不能阻止导出接口、批量接口或文件下载漏掉同一项判断。后台任务也一样;拿不到必需的权限依据时,不能直接按“允许”处理。
没有站点账号,也能得到有限许可吗
Guang 还可能想把文档导出件发给临时协作者,而不要求对方注册账号。应用可以签发难以伪造的临时下载链接,把对象、操作和有效期限定在凭证中。
这类能力凭证(Capability)让持有者获得指定能力。普通文档 ID 只是定位信息,能力链接则被设计为携带许可。转发链接也可能转交能力,因此它应只允许预定操作,到期前如何撤销也需要另外设计。
它适合临时下载、限时分享等权限范围明确的场景:接收方可以直接使用许可,减少建号与逐人配置的负担。代价是普通 Bearer 链接难以区分原接收者与获得转发的人。若要长期维护协作关系、按人审计或持续调整编辑权限,账号与资源权限记录通常更便于管理。
Amazon S3 预签名 URL 就具有 Bearer 凭证性质,其权限和有效期还受签发身份及相关策略约束。Amazon S3:Presigned URLs
无论使用账号权限还是能力凭证,应用都要解释清楚:为什么这份证明允许眼前这个操作,以及哪些变化会让它不再成立。
从一个应用到多个服务,权限依据放在哪里
一个应用里,哪些状态需要分开维护
前面的 canEditDocument 接收了三个对象,却没有说明它们从哪里来。先假设文档系统是一个单体应用:登录、会话和文档编辑都由同一个应用处理。
服务端维护的状态可以分成三类。它们可以放在同一个数据库中,但用途和生命周期不同:
| 状态及用途 | 在 Guang 的例子中保存什么 |
|---|---|
| 账号与凭据:找到账号,验证证明 | 用户 ID、账号状态、密码哈希、Passkey 公钥与绑定关系 |
| 登录会话:判断能否延续认证结果 | 用户 ID、认证时间、认证强度、到期时间和撤销状态 |
| 业务权限依据:判断此刻能否编辑 | 项目成员关系、角色、文档归属和状态 |
于是,一次编辑请求可以这样衔接:浏览器携带随机会话标识;应用查到有效会话,取得用户 ID;再读取账号、文档和成员关系,把这些事实交给授权规则。密码哈希和 Passkey 公钥留在认证环节,不需要复制进每个会话。
这个方案可以只用数据库完成。Redis 是可选的存储或加速组件,单体应用并不依赖它才能管理会话。状态都在服务端,带来的便利是应用可以主动修改和撤销;每次读取哪些状态、能否读到最新结果,仍需由应用设计。
部署多个实例,会话怎样共享
访问量增长后,应用可能部署 A、B 两个实例。Guang 在 A 完成登录,下一次请求却到达 B。如果会话只保存在 A 的进程内存中,B 就无法识别这个会话。
一种直接的设计是让两个实例使用同一个会话存储,例如数据库或 Redis。浏览器仍只携带会话标识,任一实例都可以查找相应记录。这里增加的是同一应用的运行副本,身份与授权的职责并没有变化,也没有因此必须改用 JWT。
若使用 Redis,类似 session:<随机标识> 的键可以保存会话记录,并设置过期时间。应用仍要落实会话的绝对有效期和空闲超时,不能在每次访问时无条件续期,让原本有限的会话永久存在。Redis 键不存在时,这个标识也就不能再作为有效登录的依据;读取失败则不能假定会话仍然有效。OWASP Session Management:会话过期
共享存储解决了“另一个实例找不到会话”,却没有自动解决“另一个实例仍在用旧会话”。如果实例还保留本地缓存,退出登录和撤销会话就必须覆盖这层缓存。
Redis 在这里承担哪些职责
一次编辑既要读取会话,也要取得成员关系。Redis 可以承载会话记录,也可以缓存业务数据库中的权限依据。服务端会话本身还可以关联访问权限;若把项目角色放进去,就需要把它作为业务权限的副本维护。OWASP Session Management:会话内容
先区分这些数据的职责,才能确定请求该读什么、变更该更新什么:
| Redis 中的数据 | 读取与维护责任 |
|---|---|
| 会话记录 | 根据随机标识恢复用户 ID 和认证上下文,管理过期与撤销。 |
| 成员关系或权限缓存 | 减少业务数据库查询;缺失时回源,并维护副本的新鲜度。 |
| 撤销记录或权限版本 | 判断旧凭据、旧权限快照是否还可接受,需要可靠的当前状态。 |
| 失效通知 | 提醒实例清理本地缓存,协助传播状态变化。 |
例如,会话记录在 Redis、成员关系在业务数据库中,应用就分别查询两者。若再加入成员关系缓存,授权函数可以保持原样,读取层却多了一份要维护的数据副本。下一章用撤权后的请求检验这份副本何时还能被信任。
拆成多个服务,谁确认身份、谁判断权限
进一步拆分应用时,可以让身份服务管理账号和凭据,让入口或会话服务验证浏览器会话,让文档服务管理文档及相关权限。拆分的是责任和数据归属,而不只是进程数量。
沿着 Guang 的编辑请求,一种设计是:入口验证会话,把经过验证的用户 ID 和认证上下文传给文档服务;文档服务确认调用来源可信,再取得 doc-42 的当前授权依据,执行检查和写入。外部请求自行提交的 X-User-Id: u-guang 不能直接成为内部身份。入口需要清除或覆盖这类外来字段,接收方也要验证调用方及上下文来源。
身份上下文可以通过经过认证的内部通道传递,也可以使用面向文档服务签发、由它验证的短期令牌。此时 JWT 可以只携带身份声明,文档权限仍由服务端读取。入口确认了 Guang 是谁,并不因此知道每份文档的最新共享规则。OWASP:微服务身份传递与服务级授权
下图比较登录后的请求路径。它展示一种逐步拆分方式;会话存储和业务数据的职责,在三个阶段都保持可辨认。
当多个服务需要共享复杂规则时,也可以引入统一授权服务。业务服务提供主体、资源、操作和必要上下文,授权服务作出判断,业务服务执行结果。
集中判断便于统一策略,代价是网络延迟与可用性依赖。业务服务自行判断可以减少这次远程调用,但要保持各入口规则一致。两种设计都需要考虑所用数据与缓存的新鲜度。选择时要看权限由谁维护、判断需要哪些数据,以及撤权允许滞后多久。
补充说明:判断由谁执行。 系统复杂后,可以把拦截操作的 PEP(策略执行点)、作出判断的 PDP(策略决策点)、提供属性的 PIP(策略信息点)和管理策略的 PAP(策略管理点)分开。这是职责划分,不要求部署四个独立服务。OASIS XACML 3.0
服务拆分并不要求每个服务直接读取同一套 Redis 会话记录,也不要求身份服务维护所有业务权限。通过明确的接口取得所需依据,可以避免所有服务都依赖同一份内部存储结构。若成员关系和文档分属不同服务,还需要协调权限变更与文档写入;共享一个 Redis 并不能把它们变成同一个数据库事务。
Guang 被移出项目后,旧许可怎样失效
10:03 撤销成员关系,10:04 的请求会怎样
继续运行前面的规则:把成员状态改成 removed,编辑会被拒绝。但如果接口根本没有读取新状态呢?
假设系统在 10:00 签发 JWT,把 Guang 的项目角色写进去,令牌原定 10:15 到期。资源服务只在本地验证签名和声明。10:03,管理员移除了 Guang 的成员关系;10:04,他再次提交修改。
此时令牌可以仍然有效,里面的角色却已经过时。如果改成服务端会话,但每次只读取登录时存入的角色,结果也可能相同:会话仍有效,权限快照已经过时。
把下面的代码接在前面的授权示例后运行,就能看到这一区别。这里用对象模拟两个时刻的记录,不涉及真实存储或并发:
const session = { userId: guang.id, membership: { ...membership } };
const currentMembership = { ...membership, status: "removed" };
console.log(canEditDocument(guang, document, session.membership)); // true:旧快照
console.log(canEditDocument(guang, document, currentMembership)); // false:当前记录
同一个函数、同一个用户、同一份文档,只因为数据来自不同时间,判断就不同。下面把三种路径放到同一条时间轴上:
问题出在判断依据的时间,而不只是它存在哪里。 服务端维护权限可以避免等待旧令牌到期,前提是请求确实能读到撤权结果。
让撤权结果进入每一条读取路径
最直接的办法是逐次读取当前成员关系。若为了减少查询而把权限放进会话,也可以在撤权时更新或作废受影响的会话。Guang 有多个设备会话时,处理其中一个不够;重新登录也必须从更新后的权限依据建立状态。
这里要明确“撤权完成”的时点。成员变更先写数据库,会话或缓存稍后才更新,中间就仍有窗口。应用需要等必需的更新生效,或者在此期间阻止请求继续使用旧依据。
缓存回填也会参与这个过程。假设一次查询在 10:03 前读到旧成员关系,却在管理员清除缓存后才写回 Redis,旧权限就被重新放了回来。因此,清缓存还需配合版本检查等机制,防止旧结果覆盖新状态。设置缓存有效期可以限制复用时间,但只有回源和回填也遵守新旧顺序,才能据此说明撤权最多滞后多久。
权限版本提供了一种检查办法:成员关系变更时递增版本,请求发现快照版本落后,就重新加载或拒绝使用。当前版本必须来自可靠来源;把它与权限内容一起放进长期不刷新的缓存,仍然只能读到旧结果。版本还要覆盖相关变更:只更新账号版本,不会自动捕获文档归属或项目策略的变化。
Redis Pub/Sub 可以通知各实例清理本地缓存,但采用至多一次投递,断线的订阅者可能永久错过消息。通知适合加快传播,还需配合可补偿的事件处理、重新校验或有效期约束,才能控制遗漏与延迟。Redis:Pub/Sub 投递语义
故障恢复时,还要区分“缓存没了”和“撤销事实没了”。权限缓存通常可以从业务数据库重建;会话记录丢失不能凭空恢复登录;撤销记录或版本丢失若被当作“没有撤销”,可能让旧许可重新生效。
Redis 默认异步复制,副本读取可能滞后,故障切换也可能丢失已确认的写入。持久化策略则影响恢复后保留哪些状态。要求撤权立即生效的路径,需要把这些情况纳入读取、恢复和拒绝策略。Redis:复制、Redis:持久化
对 doc-42 的编辑,如果缓存不可用,可以读取满足一致性要求的业务数据库;所需依据仍不可得时,就拒绝操作或返回暂时不可用。若业务接受短暂滞后,可以通过令牌或缓存有效期限制窗口,但必须把读取和传播路径一起纳入这个上限。
已经通过检查的编辑,撤权后还能提交吗
再把时间往前挪一点:一次编辑在 10:02:59 通过授权检查,10:03 成员关系被移除,10:03:01 才写入文档。即使之后的新请求都能读到最新权限,这次已经通过检查的操作仍可能完成。
如果业务要求“撤权完成后,旧权限支持的写入也不能再提交”,就需要让授权检查、写入和撤权遵守同一套并发规则。
例如,成员关系与文档都放在 PostgreSQL 中时,编辑事务可以对成员记录加上会阻止并发更新的行锁,重新检查权限,再持锁完成文档更新。撤权事务则更新同一条成员记录。
编辑先取得锁,撤权就等待它完成。撤权先完成,编辑就不能继续依据旧状态提交,需要拒绝或重试后重新判断。两种先后顺序都把撤权完成的时点与编辑提交协调起来。PostgreSQL:行级锁
这会引入等待和死锁处理成本,而且相关授权事实也需要相应保护;仅把两个普通查询放进事务,并不会自动建立上述顺序。若权限在另一个服务或 Redis 中、文档写在数据库里,还需要跨边界的一致性设计。后台任务也应明确权限在哪一步重新检查,以及撤权后允许已经执行到什么阶段的工作完成。
查询令牌状态,能否替代查询业务权限
过期是到达约定的有效期;撤销是在到期之前停止接受凭据。服务端会话可以删除或标记记录;只做离线验证的自包含令牌,则需要额外状态或通知渠道才能获知撤销。
OAuth 的令牌撤销协议定义了客户端怎样请求撤销,但资源服务何时拒绝旧令牌,还取决于验证和传播机制。仅删除浏览器里的凭据,也不会让攻击者保存的副本失效。RFC 7009
令牌内省(Token Introspection)提供了一条查询路径:资源服务向授权服务器询问令牌的状态和元数据。它能获得较新的有效性信息,代价是调用依赖和延迟;缓存结果又会带来状态滞后。RFC 7662 §2、§4
即使返回 active: true,也不能直接推出 Guang 仍能编辑 doc-42。授权服务器可能只知道令牌没有到期或撤销,项目成员关系却由文档系统管理。令牌状态和业务权限需要分别从各自的权威来源取得,再共同参与判断。
退出登录只是其中一种变化
Guang 退出当前设备、修改密码和离开项目,改变的是不同的依据。应用不能只提供一个“退出”按钮,就认为所有失效问题都已经解决。
| 事件 | 需要重新判断的状态 |
|---|---|
| 退出当前设备 | 本地凭据、对应会话,以及相关令牌是否需要撤销 |
| 密码或认证器泄露 | 受影响凭据、既有会话、刷新能力与新凭据绑定 |
| 被移出项目 | 成员关系、角色缓存与正在执行的敏感操作 |
| 账号停用或员工离职 | 登录入口、本地账号、会话、授权关系与同步状态 |
| 客户端或服务被停用 | API Key、客户端凭据、证书、既有令牌与分发渠道 |
例如,更换密码后是否清除其他设备的会话,需要由应用实现。数据库中的密码字段变了,不会自动通知所有服务停止接受旧会话。
至此,这条请求路径才有了完整的生命周期:Guang 获得依据后可以编辑,失去依据后也会被拒绝。接下来换一种登录来源和调用方式,看看哪些判断可以交给别的系统,哪些仍要留在文档应用中。
联邦认证与委托授权:外部系统参与哪一步
同样是跳转,取得的结果可能不同
现在给文档系统增加两个功能:Guang 用企业账号登录,或者允许排版工具读取他选定的文档。前者需要可信的认证结果,后者需要有限的访问许可。
OAuth 2.0 主要解决委托授权问题。排版工具可以取得访问令牌,不必拿到 Guang 在文档系统中的密码。RFC 6749 §1
前面已约定 Guang 获得了委托工具读取 doc-42 的权限,因此他可以在这次流程中充当资源所有者,即有权授予资源访问许可的一方。这是 OAuth 的协议角色,不等于项目中的 owner 角色。排版工具是请求访问的客户端,签发许可的是授权服务器,提供文档 API 的是资源服务器。后两项可以属于同一个产品,但职责仍然不同。
客户端也不特指浏览器。能保护自身客户端凭据的服务端应用属于机密客户端;浏览器应用等无法保守静态秘密的应用属于公共客户端。client_id 只是公开标识,把 client_secret 写进前端包不会让它变成秘密。
企业登录还需要说明“谁完成了认证”。前面提到的 OIDC 在 OAuth 2.0 上增加身份层,定义标准化的认证结果和用户信息语义。文档应用作为依赖方验证外部结果,映射到本地 Guang,再建立自己的会话。OpenID Foundation:How OpenID Connect Works
这里会遇到三种用途不同的令牌:
| 令牌 | 交给谁验证或使用 | 解决什么问题 |
|---|---|---|
| 访问令牌(Access Token) | 资源服务器 | 请求获准访问的 API 和资源 |
| 刷新令牌(Refresh Token) | 授权服务器 | 按仍有效的授权取得后续令牌 |
| 身份令牌(ID Token) | OIDC 客户端 | 验证用户认证结果 |
访问令牌不一定是 JWT。ID Token 则是 OIDC 定义的 JWT,但不能因为签名正确就拿它代替业务 API 的访问令牌。刷新令牌也只应交给相应的授权服务器,不作为普通 API 凭据传播。RFC 6749 §1.4、§1.5、OpenID Connect Core §2
自建认证与联邦认证,谁承担哪些成本
联邦认证改变的是认证责任。文档应用仍要验证收到的结果、映射本地账号,并执行自己的资源授权。
| 方式与适用场景 | 优势与主要代价 |
|---|---|
| 自建认证:独立账号体系,需要自行控制注册、登录和恢复体验 | 能直接管理凭据与认证策略。代价是自行承担凭据存储、防滥用、认证器绑定和恢复等长期责任。 |
| 联邦认证:企业员工已有统一身份,或应用需要接入既有身份提供方 | 可复用已有认证能力,减少文档应用直接处理用户凭据的范围。代价是身份提供方依赖、信任配置、账号映射及停用同步;统一登录也会扩大身份提供方故障或失陷的影响。 |
如果 Guang 使用企业账号,文档应用可以接受 OIDC 认证结果,而企业内部仍使用 Passkey。这两个选择互相衔接。OpenID Foundation:How OpenID Connect Works
OAuth 则把对排版工具的委托从 Guang 的登录凭据中分离出来,便于单独限制范围和管理撤销。代价是增加客户端登记、授权流程和令牌生命周期管理。下面先看企业登录怎样验证外部结果,再回到工具拿到许可后的资源检查。
浏览器跳回来以后,应用为什么可以建立会话
以服务端 Web 应用使用 OIDC 登录为例。浏览器先带回授权码(Authorization Code),应用再到令牌端点交换令牌。授权码是短暂、一次性使用的中间凭据。
仅有一个码还不够。应用要确认它属于当前浏览器发起的流程,并把换码动作与发起方关联起来。PKCE(Proof Key for Code Exchange)为这次交换增加一个临时秘密:先提交由秘密计算出的挑战,换码时再证明自己知道原始值。
整个过程分为七步:
- 应用建立登录事务,保存预期身份提供方、回调地址和与当前浏览器的关联信息。生成随机的
state、nonce和 PKCE 验证值。 - 浏览器跳转到预先信任的授权端点,带上应用标识、注册的回调地址、
openid等范围,以及事务参数和 PKCE 挑战。 - 身份提供方认证 Guang,按请求、已有授权或组织策略决定是否批准,需要时征求他的同意。
- 浏览器带着授权码和
state返回。应用核对回调与当前浏览器事务的对应关系。 - 应用服务端向对应令牌端点提交授权码、PKCE 验证值及其他必需参数。机密客户端同时完成客户端认证。
- 授权服务器检查授权码的归属、回调绑定、有效性和 PKCE 证明,成功后返回令牌。
- 应用验证 ID Token 的签名、签发者、客户端受众、有效期、
nonce及该流程的其他条件。验证通过后映射本地账号,建立会话。
图中的编号与这七步对应。步骤 5 证明应用自身身份,步骤 7 验证 Guang 的认证结果。收到回调或解码出用户信息,都不能代替后一步验证。OpenID Connect Core:Authorization Code Flow
采用 S256 时,PKCE 挑战由随机验证值 code_verifier 计算:
code_challenge = BASE64URL(SHA256(code_verifier))
换码时,授权服务器根据提交的验证值重新计算并比较挑战。因此,只窃取授权码、没有验证值的人,无法仅凭该码完成交换。这个临时秘密绑定一次流程,不会赋予公共客户端保护长期秘密的能力。RFC 7636 §4
在上述设计中,state 关联回调与浏览器事务,nonce 关联 ID Token 与认证请求,PKCE 关联授权码与临时秘密。它们的作用有重叠,规范允许满足条件的替代保护,不能把示例当成所有流程统一的参数清单。
按 RFC 9700 的安全基线,授权码流程中的公共客户端必须使用 PKCE,机密客户端推荐使用。资源所有者密码凭据授权被禁止,隐式授权也不适合作为新方案的默认选择。可信端点、回调匹配和流程关联同样需要正确实现。RFC 9700 §2.1、§2.4
到这一步,外部认证结果已经转换为本地 Guang 的会话,后续编辑便可接回前面的授权函数。
本地会话的有效期由文档应用管理。仅用 OIDC 完成登录时,它不必随身份提供方签发的访问令牌到期而结束。RFC 10017:本地会话与 OAuth API 访问
访问令牌到期后,排版工具怎样继续调用
现在回到排版工具:它需要持续调用文档 API,读取 Guang 委托的 doc-42。访问令牌到期后,工具就不能再用这张旧令牌访问资源。如果授权服务器签发了刷新令牌,工具可以在授权仍然有效时换取新的访问令牌,无需每次都让 Guang 重新参与授权流程。
刷新令牌代表继续取得许可的能力,也延长了信任链。它需要更严密地保存和控制,但不会让已经被盗的访问令牌自动失效。
刷新令牌轮换在刷新成功时签发新值,并使旧值失效。授权服务器保留新旧令牌的关系,再次收到旧值时,就能识别重用风险并撤销相关的当前刷新能力。如果只是返回新字符串,却继续接受所有旧值,就没有形成这种保护。
RFC 9700 要求公共客户端的刷新令牌采用发送者约束或轮换来检测重放。实现还需处理正常并发和网络重试,避免容忍策略破坏旧值重用检测。RFC 9700 §4.14
Guang 离开项目后,后续刷新也要依据仍有效的授权决定许可范围;已签发令牌的处理则接入前面的撤权路径。
排版工具得到读取范围,就能读取所有文档吗
工具拿到 documents:read,说明许可包含相应的范围(scope),不代表文档系统中的每份文档都向它开放。资源服务仍需确认令牌面向哪个 API、代表谁或哪个应用,以及 doc-42 是否落在实际许可内。
这里有三个不同层次的约束:audience 限定预期接收者,scope 限定许可范围,文档服务把实际委托与本地资源策略一起检查。如果 Guang 只委托工具读取 doc-42,即使他自己能读 doc-43,工具也不能因此读取后者。Guang 被移出项目后,旧的读取范围同样不能单独支撑继续访问。
上游令牌由浏览器保管,还是交给 BFF
取得令牌以后,还要决定由谁持有它、由谁调用资源 API。浏览器可以直接取得访问令牌并构造请求,这减少了一层业务代理,也把凭据保护和令牌生命周期管理放到了浏览器中。
另一种设计是把上游调用集中到 BFF(Backend for Frontend,面向前端的后端)。在这里讨论的架构中,浏览器持有应用会话 Cookie,BFF 保存上游访问令牌和刷新令牌。它用访问令牌调用资源 API,需要刷新时,再把刷新令牌交给授权服务器的令牌端点。
这样,浏览器代码通常不接触上游令牌,但 BFF 增加了会话管理、令牌保管和代理范围控制的责任。浏览器侧仍需要 CSRF 防护;XSS 也可能借会话调用 BFF,不能因为令牌移到后端就忽略页面安全。RFC 10017:BFF 的职责与安全边界
下图只比较取得令牌后的业务调用。无论谁保管凭据,资源 API 都仍需检查当前文档权限。
企业登录统一了,账号状态会自动统一吗
Guang 已在企业身份提供方登录,再进入受信任的文档应用时,可以不必重新输入凭据。这种跨应用复用认证体验的效果称为单点登录(Single Sign-On,SSO)。
OIDC 可以支持它;已有企业系统也可能使用 SAML,以断言传递认证结果。文档应用验证可信签发方的签名、断言受众、有效条件和响应关联,再建立本地会话。OASIS:SAML 2.0 Technical Overview
选择协议时,已有身份体系会直接影响接入成本。OIDC 面向 Web、移动和原生客户端,可复用 OAuth 与 JWT 的工具链,需要正确处理流程和令牌用途。SAML 适合接入已有 SAML 体系的企业 Web 应用,能复用配置与信任关系,代价是 XML、断言和签名验证的集成复杂度。OpenID Foundation:OIDC 与 SAML
但统一登录入口,不等于账号生命周期已经同步。如果需要查询 Guang 所属的组织,企业目录可以通过 LDAP 提供信息;LDAP 也有 Bind 等认证操作,但它本身不是浏览器联邦登录流程。若要跨系统创建、更新、停用账号和管理组,则是 SCIM 处理的协作问题。RFC 4511、RFC 7644
员工离职后,目录、本地账号、成员关系和会话要配合失效。退出也一样:退出文档应用通常只处理本地会话。如果身份提供方仍保持登录,下一次跳转可能再次建立应用会话。是否同步退出其他应用,需要额外的会话协调。
Guang 没有打开页面时,服务怎样获得许可
后台任务证明的是谁
文档的定时导出、Webhook 或自动排版也会调用 API。应用首先要分清:这是服务以自身身份操作,还是代表 Guang 执行一项委托。服务通过认证,并不能自动回答它对 doc-42 有什么权限。
一种简单接入方式是 API Key。平台签发秘密,并将其映射到应用或账号;调用方提交这个值,接收方检查归属、状态和权限。它适合范围明确的程序化集成,优势是接入直接;经常具有 Bearer 性质,也意味着泄露后容易被复制使用。因此,还需要限制权限、轮换、撤销和可靠分发。公开应用 ID 与秘密 API Key 不能混用。OWASP REST Security:API Keys
如果还需要核验调用方提交的消息内容,例如接收 Webhook,就可以对约定内容产生签名。HMAC 是基于哈希的消息认证码:调用方用共享密钥计算,接收方用同一密钥重算并比较。它提供完整性与持钥证明,但验证方也能生成同样的认证码。非对称签名则让调用方持有私钥,接收方只用公钥验证。
例如,自动排版任务要提交新的文档正文,双方就要约定方法、地址、必要头部和正文摘要哪些纳入计算。签名只保护实际签入的内容。把证明与请求绑定的代价,是双方要维护一致的消息规范化规则和密钥,并结合时间、随机标识与服务端状态拒绝重放。RFC 9421:HTTP Message Signatures
补充说明:旧接口里的 HTTP 认证。
Authorization请求头也可能使用 Basic、Digest 或 Negotiate。Basic 对用户名和密码做 Base64 编码,不提供加密;Digest 使用挑战响应;Negotiate 可承载基于 SPNEGO 的 Kerberos 等认证协商。它们负责交换或验证凭据,后续仍需执行资源授权。RFC 7617、RFC 7616、RFC 4559
连接证明了服务身份,能否推出 Guang 的身份
普通 HTTPS 通常由客户端验证服务器。双向 TLS(mTLS)让客户端也提供证书并证明掌握相应私钥。接收方验证证书信任链或约定的信任关系,再把证书身份映射到调用主体。
这能证明连接另一端的服务身份,却不会自动证明它代表 Guang。如果 TLS 在网关终止,后端还要通过可信方式取得网关确认的身份。OAuth 可以用 mTLS 认证客户端,也可以把访问令牌绑定到证书;两者解决的是不同问题。RFC 8705
mTLS 适合能够统一管理证书的服务通信,优势是把服务认证放在连接层。代价是证书签发、信任分发、轮换和终止位置的管理;经过代理后,仍要保证业务服务收到可信的身份上下文。
服务实例频繁创建和销毁时,长期复制静态 Key 又会带来分发与回收困难。工作负载身份把运行中的服务作为主体,根据运行环境等证明签发凭据,并管理更新。SPIFFE 提供这类身份标识和可验证身份文档。它适合动态实例环境,减少手工分发长期秘密,但也增加身份签发与运行环境集成的运维责任。它解决服务如何取得身份依据,资源权限仍由接收方决定。SPIFFE Overview
服务以自身身份,怎样取得访问令牌
机密客户端可以使用 OAuth 的 Client Credentials 授权,以自身身份或事先约定的授权范围取得访问令牌。这样的令牌不能被解释为“Guang 刚刚完成了登录”。RFC 6749 §4.4
补充说明:设备输入受限时。 如果调用设备难以输入凭据,但仍需要 Guang 授权,可以使用 Device Authorization(设备授权),让他在另一台设备上完成交互。这改变了用户参与流程的位置,并没有省略用户授权。RFC 8628
因此,一个服务可以用 mTLS 证明自身身份,用访问令牌说明委托范围,再由文档系统判断能否操作 doc-42。这些证明各自回答一个问题,应用负责把答案连接起来。
回到实践:让每次编辑都走完判断
现在回到浏览器里的 Guang。文档应用可以自己验证凭据,也可以接受企业认证结果;可以维护服务端会话,也可以验证调用方带来的令牌。先确定谁负责证明身份、谁持有凭据、谁判断文档权限,才能把这些机制组合起来。
| 场景与衔接方式 | 文档系统仍需负责什么 |
|---|---|
| 自有账号与应用:密码或 Passkey → 本地会话 | 会话生命周期与具体资源权限。 |
| 浏览器直接访问资源 API:授权码与 PKCE → 相应令牌 | 浏览器凭据保护、API 权限与失效处理。 |
| BFF:浏览器会话 → 后端保管并使用上游令牌 | 请求保护、代理范围与本地会话、上游令牌的生命周期。 |
| 企业统一登录:OIDC 或 SAML → 外部身份映射 → 本地会话 | 应用权限、账号同步与离职失效。 |
| 服务间调用:服务凭据、mTLS 或工作负载身份 → 调用许可 | 服务权限、委托边界、轮换与审计。 |
单体应用可以先用数据库维护会话和业务权限。增加实例后共享会话,拆分服务后明确身份传递与授权执行;需要加速时再引入 Redis,并为其中的副本设计失效路径。这些部署选择不要求把 JWT 存进浏览器,也不会替代资源授权。
最终,PATCH /api/documents/doc-42 应沿着同一条判断路径执行:校验会话或令牌,建立主体;加载文档和当前成员关系,判断资源与字段权限;确认状态仍允许修改,再完成写入。Guang 换用企业登录、服务代他调用,或者应用改用 JWT,都只是改变其中一部分依据。
检查一次被拒绝或被错误放行的请求时,也可以沿这条路径追问:账号是否映射正确,证明是否验证完整,会话是否仍有效,权限是否落到这份资源,以及判断是否已经过时。这些问题把分散的机制重新连成了一次可解释的操作。
身份与访问控制由此回应了上一篇中的身份冒用、会话滥用和越权挑战。浏览器执行、服务端输入、业务逻辑和运行环境的防护,将在后续文章继续展开。