上一篇的安全挑战地图中,我们把身份冒用、会话劫持和越权放在同一条请求路径上观察。它们发生的位置不同,却可能带来同一个结果:一份文档被不该修改它的人改掉了。

假设 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 注册时绑定公钥与账号,认证时通过浏览器和认证器完成挑战签名与服务端验证

这份证明最终依赖注册时的绑定。如果攻击者能通过恢复入口,把自己的 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:浏览器保存数据,并按规则随请求发送。下面是 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

编码后的头部.编码后的声明.编码后的签名或消息认证码

头部描述算法等信息,声明可以携带主体、签发者、接收者和有效期。接收方验证这些信息后,可以在本地完成部分判断,减少逐次查询中央状态的需要。

但“能解码”还不是“可信”。接收方必须用预先配置的算法和可信密钥验证,并检查 issaud 是否符合预期。必需声明、expnbf 和用途,则按令牌类型及协议检查。同样是 JWT,也不能把一个入口接受的凭据直接交给另一个入口使用。RFC 8725 §3

签名型 JWT 的头部和声明可以被读取。JWE(JSON Web Encryption,JSON Web 加密)通过加密保护机密性,并提供完整性校验;JWT 也可以采用这种封装。判断内容是否保密,要看具体封装,不能只看“JWT”这个名称。RFC 7516 §1RFC 7519 §3

下面把两种验证路径放在同一张图中。引用型凭据需要查询权威状态;自包含声明可以在本地校验。两者可以混合使用,JWT 也能配合在线查询或撤销记录。

凭据通过 Cookie 或请求头传输后,分别查询权威状态或验证自包含声明,再进行业务授权

对 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 论文

下图把直接许可、角色、属性和关系画在同一份文档上,帮助观察判断分别从哪里开始。

ACL、RBAC、ABAC 和 ReBAC 对 Guang 编辑项目文档这一许可的四种表达方式

授权模型的优劣,怎样对应业务需求

模型的选择要看权限主要随什么变化。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:当前记录

同一个函数、同一个用户、同一份文档,只因为数据来自不同时间,判断就不同。下面把三种路径放到同一条时间轴上:

移除成员后,旧 JWT 角色、Session 中的旧权限快照与当前成员关系产生不同编辑结果

问题出在判断依据的时间,而不只是它存在哪里。 服务端维护权限可以避免等待旧令牌到期,前提是请求确实能读到撤权结果。

让撤权结果进入每一条读取路径

最直接的办法是逐次读取当前成员关系。若为了减少查询而把权限放进会话,也可以在撤权时更新或作废受影响的会话。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.5OpenID Connect Core §2

自建认证与联邦认证,谁承担哪些成本

联邦认证改变的是认证责任。文档应用仍要验证收到的结果、映射本地账号,并执行自己的资源授权。

方式与适用场景 优势与主要代价
自建认证:独立账号体系,需要自行控制注册、登录和恢复体验 能直接管理凭据与认证策略。代价是自行承担凭据存储、防滥用、认证器绑定和恢复等长期责任。
联邦认证:企业员工已有统一身份,或应用需要接入既有身份提供方 可复用已有认证能力,减少文档应用直接处理用户凭据的范围。代价是身份提供方依赖、信任配置、账号映射及停用同步;统一登录也会扩大身份提供方故障或失陷的影响。

如果 Guang 使用企业账号,文档应用可以接受 OIDC 认证结果,而企业内部仍使用 Passkey。这两个选择互相衔接。OpenID Foundation:How OpenID Connect Works

OAuth 则把对排版工具的委托从 Guang 的登录凭据中分离出来,便于单独限制范围和管理撤销。代价是增加客户端登记、授权流程和令牌生命周期管理。下面先看企业登录怎样验证外部结果,再回到工具拿到许可后的资源检查。

浏览器跳回来以后,应用为什么可以建立会话

以服务端 Web 应用使用 OIDC 登录为例。浏览器先带回授权码(Authorization Code),应用再到令牌端点交换令牌。授权码是短暂、一次性使用的中间凭据。

仅有一个码还不够。应用要确认它属于当前浏览器发起的流程,并把换码动作与发起方关联起来。PKCE(Proof Key for Code Exchange)为这次交换增加一个临时秘密:先提交由秘密计算出的挑战,换码时再证明自己知道原始值。

整个过程分为七步:

  1. 应用建立登录事务,保存预期身份提供方、回调地址和与当前浏览器的关联信息。生成随机的 statenonce 和 PKCE 验证值。
  2. 浏览器跳转到预先信任的授权端点,带上应用标识、注册的回调地址、openid 等范围,以及事务参数和 PKCE 挑战。
  3. 身份提供方认证 Guang,按请求、已有授权或组织策略决定是否批准,需要时征求他的同意。
  4. 浏览器带着授权码和 state 返回。应用核对回调与当前浏览器事务的对应关系。
  5. 应用服务端向对应令牌端点提交授权码、PKCE 验证值及其他必需参数。机密客户端同时完成客户端认证。
  6. 授权服务器检查授权码的归属、回调绑定、有效性和 PKCE 证明,成功后返回令牌。
  7. 应用验证 ID Token 的签名、签发者、客户端受众、有效期、nonce 及该流程的其他条件。验证通过后映射本地账号,建立会话。

图中的编号与这七步对应。步骤 5 证明应用自身身份,步骤 7 验证 Guang 的认证结果。收到回调或解码出用户信息,都不能代替后一步验证。OpenID Connect Core:Authorization Code Flow

OIDC 授权码流程的七步时序:浏览器跳转、服务端令牌交换、验证用户认证结果与建立本地会话

采用 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 都仍需检查当前文档权限。

浏览器直接持有访问令牌调用 API,与浏览器持有会话 Cookie 经 BFF 使用上游令牌的架构对比

企业登录统一了,账号状态会自动统一吗

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 4511RFC 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 7617RFC 7616RFC 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,都只是改变其中一部分依据。

检查一次被拒绝或被错误放行的请求时,也可以沿这条路径追问:账号是否映射正确,证明是否验证完整,会话是否仍有效,权限是否落到这份资源,以及判断是否已经过时。这些问题把分散的机制重新连成了一次可解释的操作。

身份与访问控制由此回应了上一篇中的身份冒用、会话滥用和越权挑战。浏览器执行、服务端输入、业务逻辑和运行环境的防护,将在后续文章继续展开。