fedorthinks

速成课 · No. 08

「auth」这一个词背后,藏着两个问题。身份认证证明你是谁;授权决定你被允许做什么。把它们混为一谈、漏掉任何一个,或者轻信客户端,门就开着了。

22 个概念 · 每个配一张图示 · 末尾附速查表

§ 01

「auth」这个词背后,藏着两个不同的问题。大多数安全 bug,都来自把它们混淆——或者答了一个、忘了另一个。

Authentication (authN)——你是谁?

在机场出示你的护照——证明你真的就是你所声称的那个人。

这就是 Authentication(authN,身份认证) 这个概念,意思是证明身份:一次登录、一把 passkey、一个写着「这确实是 Alice」的令牌。它通常每个会话只发生一次。 做错了,冒名者就能顶着别人的身份进来。但证明你是谁,并没有说明你能做什么——那是另一个完全不同的问题。

Credentialpassword · passkey · tokenVerifyAuthNIdentityuser = Aliceanswers one question: WHO is this?
authN 把凭证(密码、passkey、令牌)变成一个身份——它只回答「这是谁?」这一个问题。
ConceptAuthentication (authN)· 证明身份
是什么
确认用户就是他所声称的那个人。
怎么运作
用户出示凭证(密码、passkey、令牌);系统验证它,并确立一个身份。
例子
邮箱加密码登录:「这是 Alice」。
陷阱
知道某人是谁,并不等于知道他能做什么。
关联

喂给 Authorization;通过 Password Hashing、MFA、Passkeys、OIDC 来完成。

Authorization (authZ)——你被允许做什么?

酒店房卡——它不说明你叫什么,但能打开你的房间,打不开别的房间,也打不开经理办公室。

这就是 Authorization(authZ,授权) 这个概念:决定一个身份被允许做什么——读这个、改那个、绝不碰管理后台。和 authN 不同,它必须在每个请求上都检查,因为一个已登录的用户可能碰上百种不同的东西。知道某人是谁,不等于允许他做任何特定的事。

WhoAlice, editorActioneditResourcedoc #42Policy checkAuthZAllowDenyon every request
authZ 接收「谁、什么操作、哪个资源」——并在每个请求上决定:放行或拒绝。
ConceptAuthorization (authZ)· 决定权限
是什么
决定这个身份能对这个资源做什么。
怎么运作
每个请求上:谁 + 操作 + 资源 → 规则 → 放行或拒绝。
例子
编辑 Alice 可以编辑文档 #42,但不能打开管理后台。
陷阱
只检查一次就忘了。必须在每个请求上、在服务端检查。
关联

模型——Least Privilege、RBAC、ABAC、Ownership Check;失误——Broken Access Control。

两层——永远别把它们合并

夜店:保镖在门口查一次你的证件,而酒保每次你点酒时仍会确认你成年。两次检查,两个时刻。

经典的错误是把两者融合——证明一次身份就永远信任它,或者把所有权限都塞进登录令牌里再也不复查。authN 发生在大门口;authZ 发生在里面的每一扇门。 把它们分开,并且不停地检查第二个。

front door: who are you?every inner door: may you?UserAuthNonce per loginGET /orders/7AuthZ checkPUT /docs/3AuthZ checkDELETE /users/9AuthZ check
authN 是一扇前门。authZ 是每扇内门上的检查——也就是每个请求上的检查。

Don't Roll Your Own Auth——永远别自己造

你不会在车库里用铁皮焊自己的银行金库——你会买一个由只干这一行的人造好并测试过的。

这就是 Don't Roll Your Own Auth 这条实践。身份是其他一切赖以站立的地基,而且极其容易在细微处出错。用经过验证的标准和库——成熟的协议、久经考验的认证库或服务商——而不是你自己发明的聪明方案。安全史上铺满了自制的认证方案:看着没问题,直到突然就不行了。

PracticeDon't Roll Your Own Auth· 标准重于自造
问题
自制的认证看着没问题,直到有人把它攻破。
思路
使用经过验证的协议、库和服务商。
例子
不用自己的令牌方案——用 OIDC 和你框架自带的认证。
代价
你得学别人的工具,并把它正确地接进来。
何时使用
几乎总是。
关联
通向 Auth Provider;标准——OAuth 2.0、OIDC。

地图:六组共 22 个概念

一串钥匙:先看到整串,再学会哪把钥匙开哪扇门。

本课的概念分成六组。每组回答各自的问题:这两个「auth」是什么、如何证明身份、如何保持登录、如何不用密码授予访问、如何决定权限,以及这一切通常怎么崩坏。

Two questionsAuthenticationAuthorizationNo DIY authProving identityPassword hashingMFAPasskeysStaying logged inSessionJWThttpOnly cookieDelegated accessOAuth 2.0OIDCAPI keyPermissionsLeast privilegeRBACABACOwnership checkFailure modesIDORToken leakageTrusting the client
六组概念——第 1–6 章依次讲它们。第 7 章是如何选择,外加速查表。

下面每个概念都得到同样的待遇:它的英文名称(你查文档时要搜的术语)、一幅生活中的画面、一张图示,以及一张带字段的卡片。末尾是收录所有概念的速查表。

身份认证是大门。授权是里面的每一扇门。漏掉第二个,第一个就毫无意义。

§ 02

身份认证归根结底就是证明你是你。各种因素,从脆弱但通用的密码,到现代的、抗钓鱼的 passkey。

Password Hashing——密码,脆弱的默认选项

和门卫共享的一个暗号——管用,直到有人偷听到、猜中,或者你到处都用同一个。

密码是通用的,也是最常见因素里最弱的:会被复用、会被猜中、会被钓鱼。如果非用不可,不容商量的规则是绝不把它们存成文本。要存一个缓慢的、加盐的哈希(Argon2、bcrypt)——这就是 Password Hashing——这样被盗的数据库也不会交出所有人的密码。(更多内容见「失误」那一章。)

Passwordhunter2+ saltrandom, per userSlow hashArgon2 · bcryptDB stores$argon2id$…Stolen DBonly hashescracking is slow, one user at a time
密码 + 随机盐 → 慢哈希 → 只存哈希。被盗的数据库交不出任何密码。
PracticePassword Hashing· 加盐的慢哈希
问题
密码存成文本:一次泄露,攻击者就全拿到了。
思路
不存密码,存一个带盐的慢哈希——每个用户的盐都不同。
例子
登录时,对输入的密码计算 Argon2/bcrypt 并比对。
代价
登录稍微慢一点——这是故意的。
何时使用
只要你存密码,就总是用。
关联
反面例子是 Plaintext / Fast Hash;再在上面加 MFA。

MFA——你知道的、你拥有的、你本身

银行的保管箱需要你的钥匙和银行的钥匙。单独一把什么也打不开。

MFA(Multi-Factor Authentication,多因素认证),即双因素,要求在密码之外再提供第二个证明——你拥有的东西(应用里的验证码、安全密钥)或你本身的特征(指纹)。这样即使密码被盗,单靠它也没用。验证器应用的验证码(TOTP)很可靠;短信验证码较弱(可被拦截),但总比没有强。MFA 是你能加上的、最大也最便宜的一项安全收益。

Something you knowpasswordSomething you haveTOTP app · security keySomething you arefingerprint · faceAccess2+ factorsany 2 of 3SMS code: weak (interceptable)
三类因素:知道、拥有、本身。登录至少需要两个不同的因素。
PracticeMFA· Multi-Factor Authentication, 2FA
问题
一个密码被盗 = 账户被完全接管。
思路
要求另一类的第二因素:「拥有」或「本身」。
例子
密码 + 验证器应用里的六位 TOTP 验证码。
代价
登录多一步;短信验证码可能被拦截。
何时使用
凡是重要的地方;管理员账户必须启用。
关联
加固 Password Hashing;Passkeys 是下一步。

Passkeys——无密码的未来

一把只在它被配制的那一扇门上有效的钥匙——伪造网站、复印锁的照片,钥匙就是转不动。

Passkeys(基于 WebAuthn 标准)用绑定到你的设备和真实网站的一对加密密钥取代密码,所以没有什么可被钓鱼、复用或泄露的——Apple、Google 和 Microsoft 现在都默认采用。私钥永远不离开你的设备;网站只会看到一个签名。认证正在朝这个方向走:没有什么可偷。

Your deviceholds private keyreal-site.comstores public keyfake-site.comwrong origin1. challenge2. signature✗ no signature herekey never leaves device
网站发来一个挑战,设备用签名回应。位于另一个地址的钓鱼网站拿不到签名。
ConceptPasskeys· WebAuthn, 无密码登录
问题
密码会被钓鱼、被复用、被泄露。
思路
一对密钥:私钥留在设备上,网站持有公钥。登录就是一次签名。
例子
用指纹或 Face ID 登录,而不是输入密码。
代价
需要设备支持,以及一条备用的恢复途径。
何时使用
在用户使用现代设备的地方,作为主要登录方式。
关联
取代 Password Hashing;单独使用时,比短信 MFA 更强。

密码是你可能丢掉的秘密。passkey 是你丢不掉的证明。无论哪种,都再加一个第二因素。

§ 03

HTTP 在两次请求之间会忘掉你,所以登录之后,系统需要一种办法继续认出你。有两种做法,各有真实的取舍。

Stateless HTTP——HTTP 没有记忆

一家店,店员你一转身就把你忘了——所以你需要一张票,在下一步证明自己已经付过款。

HTTP 是无状态的:每个请求都是独立的;服务器不记得你一秒前刚登录过。所以认证之后,会发给你一样东西——一个 cookie 或令牌——你随每个请求一起发送,表示「还是我」。下面的一切,都是做到这一点的两种方式。(这就是协议课里讲的无状态。)

Request 1+ cookie / tokenit's still meRequest 2+ cookie / tokenit's still meRequest 3+ cookie / tokenit's still meServerforgets between calls
服务器不记得任何之前的请求,所以每个请求都得自带一个 cookie 或令牌。
ConceptStateless HTTP· 请求之间没有记忆
是什么
服务器不记得客户端之前的请求。
后果
登录之后,你需要一张随每个请求一起走的「通行证」。
例子
浏览器会自动把 cookie 附在发往该网站的每个请求上。
关联
两种答案——Session 和 JWT。

Session——服务器来记

衣帽间——你手里拿着一张编了号的小票,所有真实信息都在柜台后面、记在那个号码下。

用会话(session)时,服务器保存你是谁,只给你一个 cookie,里面只有一个随机的会话 id。每次请求,它都去查这个 id。服务器是事实的来源,所以注销或封禁某人是即时的——删掉会话就行。代价是服务器必须存储并查询这份状态。大多数网页应用的正确默认选择。

Browsercookie: sid=8f3a…Serverlooks up the sidSession store8f3a → AliceDelete sessionlogout / ban = instantrevoke
cookie 里只有一个 id。服务器在会话存储里查它。删掉会话——用户立刻被踢出。
PatternSession· 服务端会话
问题
HTTP 在请求之间不记得你是谁。
思路
服务器保存登录数据;客户端只持有一个随机的会话 id。
例子
Cookie sid=8f3a… → 在存储里,8f3a → Alice。
代价
服务器需要一个存储,并在每个请求上做一次查询。
何时使用
普通的网页应用。这是默认选择。
关联
替代方案是 JWT;id 存放在 httpOnly Cookie 里;撤销是即时的。

JWT——令牌自己带着真相

音乐节手环,你的访问等级已经印在上面并加了防篡改封条——保安检查的是封条,而不是来宾名单。

JWT(JSON Web Token) 是客户端持有的一个签名令牌,它包含身份及其 claims(声明);服务器只需验证签名,不用查任何东西。这让它无状态,也便于跨服务扩展——但难以撤销: 泄露的令牌在过期之前一直有效。所以要把有效期设短,并搭配一个 refresh token。非常适合 API 和微服务;对简单的网页应用来说是杀鸡用牛刀。

headeralgpayloadsub: alice · role · expsignaturesigned by issuer..Clientholds the JWTAPIverify signatureLeaked JWTvalid until expno lookup · statelessso: short expiry + refresh token
JWT = header.payload.signature。API 不用数据库就能校验签名。泄露的令牌活到 exp 为止——所以要短有效期加 refresh token。
PatternJWT· JSON Web Token
问题
服务很多,让每一个都去访问共享的会话存储,代价太高。
思路
把身份和权限放进令牌本身,并签名。
例子
载荷 sub: alice, role: editor, exp: …;每个服务自己验证签名。
代价
难以撤销:泄露的令牌在过期前一直有效。
何时使用
API 和微服务——短有效期的 JWT + 一个 refresh token。
关联
Session 的替代方案;威胁是 Token Leakage。

httpOnly Cookie——存在哪里很重要

钥匙锁在抽屉里,与钥匙用胶带粘在前门上——同一把钥匙,安全性天差地别。

浏览器里的令牌应当放在 httpOnly cookie 里(JavaScript 读不到),而不是 localStorage——那里任何被注入的脚本都能偷走它。存放位置本身就是安全的一部分:弄错了,一个 XSS bug 就会把所有会话拱手送给攻击者。你如何传输和存放凭证,与凭证本身同样重要。

httpOnly cookieJS can't read itlocalStorageany script can read itInjected scriptXSSsteals token✗ blocked
被注入的脚本(XSS)能读 localStorage,却读不了 httpOnly cookie。
PracticehttpOnly Cookie· JavaScript 读不到的 cookie
问题
放在 localStorage 里的令牌,会被任何被注入的脚本偷走。
思路
把令牌放进带 httpOnly 标志的 cookie——JS 看不见它。
例子
Set-Cookie: sid=…; HttpOnly。
代价
几乎没有——它是浏览器里正确的默认做法。
关联
存放 Session id 或 JWT;防范 Token Leakage。

会话:服务器记着,也能立刻忘掉你。JWT:令牌记着,你很难把它收回来。

§ 04

很多时候你根本不想经手密码——你想让用户用他们已有的账户登录,或者让一个服务代表另一个服务行事。OAuth2 和 OIDC 就是干这个的。

OAuth 2.0——不共享密码就授予访问

代客泊车钥匙:能发动汽车、打开车门,但打不开后备箱和手套箱——你交出的是有限的访问权,而不是整串钥匙。

这就是 OAuth 2.0 协议。它让用户可以授予你的应用对其在另一个服务上的账户的有限访问权——你的应用读取他们的 Google 日历——而不必把 Google 密码交给你。 对方服务会给你的应用一个受 scope(范围)限制的 access token(「读日历,别的不行」)。它讲的是委托授权,不是身份。

UserAuth serverGoogleYour appclientResource APICalendar1. logs in there + consentsyour app never sees the password2. access token · scope: read calendar3. call the API with the token
用户在 Google 登录并同意。应用拿到一个范围很窄的令牌,并用它调用 API。它从未见过密码。
ProtocolOAuth 2.0· 委托授权
问题
一个应用需要访问某人在别处的账户,但不能拿到密码。
思路
账户所属的服务向应用签发一个带窄 scope 的 access token。
例子
你的应用读取用户的 Google 日历——也只读这个。
代价
协议复杂;手工搞这套令牌流程很危险。
何时使用
在另一个服务里代表用户行事。
关联
它不说用户是谁——那是它之上的 OIDC。

OIDC——还能告诉你「你是谁」的 OAuth2

代客泊车钥匙上又夹了一张带照片的证件——不只是「这把钥匙能开什么」,还有「它属于这个人」。

单靠 OAuth2,你只知道应用可以访问什么,而不知道用户是谁。OpenID Connect(OIDC) 在其上加了一层很薄的身份层——一个带有已验证 claims 的 ID token(这是 Alice,这是她的邮箱)。「用 Google 登录」和单点登录(SSO) 靠的就是它。当你想要社交登录或 SSO 时,OIDC 就是标准。

"Log in with Google" · SSOOpenID Connect (OIDC)ID token: WHO the user isOAuth 2.0access token: WHAT the app may reachidentityaccess
OIDC 是 OAuth 2.0 之上的一层薄层:在 access token(「允许什么」)旁边,多了一个 ID token(「这是谁」)。
ProtocolOIDC· OpenID Connect
问题
OAuth2 授予访问权,但不说用户是谁。
思路
加一个带已验证身份数据的 ID token。
例子
「用 Google 登录」按钮;通过 SSO 登录工作工具。
代价
你依赖一个外部的身份提供方。
何时使用
社交登录和单点登录。
关联
建立在 OAuth 2.0 之上;做 Authentication 的一种方式。

API Key——简单的机器凭证

你给一位可信承包商的备用钥匙——上面没有名字,只是「谁拿着它,谁就能进来」。

API key 是一长串秘密字符串,标识的是一个程序,而不是一个人——用于服务之间和程序化的访问。简单有效,但它就是凭证:谁找到它,谁就能进来。所以要限定范围、定期轮换,并且绝不提交到 git,也绝不放进 URL。 适合机器;不能代替真正的用户身份。

Service Aa program, not a personAPIchecks key + scopeheader: X-API-Key: sk_…✗ never in git✗ never in a URL↻ scope + rotate
程序把密钥放在 header 里发送;API 检查密钥及其范围。绝不进 git,绝不进 URL,定期轮换。
ToolAPI Key· 机器凭证
问题
一个程序要访问某个 API,没有人,也没有登录。
思路
一个标识该程序的长秘密。
例子
后端把密钥放在 header 里调用支付 API。
代价
密钥就是访问权:找到它就能进来。
何时使用
机器对机器。不用于登录真人。
关联
用 Least Privilege 限定范围;主要威胁是 Token Leakage。

OAuth2 不要密码就借到访问权。OIDC 再加上「这是他们本人」。用标准——别自己发明令牌流程。

§ 05

一旦知道某人是谁,你就要决定他能碰什么。这些模型从粗粒度的角色一直到细粒度的策略——而所有模型之下的规则都是默认拒绝。

Deny by Default / Least Privilege——除非允许,否则禁止

新员工的工牌,在每扇门被明确授权之前什么也打不开——而不是什么都能开,直到有人想起来锁上其中几扇。

授权的基础是两条规则。Deny by default(默认拒绝): 除非明确允许,一切都被禁止。Least privilege(最小权限): 每个身份只获得它所需的最低限度的访问,不多给。默认开放的系统会泄露——总有人忘了限制新的端点。默认关闭的系统则安全地失效。从「不」开始,有意识地授权。

RequestExplicit grant?Allowthe minimum neededDenythe defaultyesno / unknown
有明确授权——放行,而且只给所需的最低限度。没有授权,或不明确——拒绝。
PrincipleLeast Privilege· 默认拒绝
问题
有人忘了给新端点上锁——结果它对所有人敞开。
思路
默认「不」;只授予所需的最低限度。
例子
新员工只能访问他自己的项目。
代价
你得更频繁地明确授予权限。
何时使用
总是——它是每个权限模型的基础。
关联
是 RBAC、ABAC、Ownership Check 的地基。

RBAC——按角色授权

剧院里,你的票种——池座、楼座、后台通行证——决定你能去哪里,而不用点你的名。

RBAC(Role-Based Access Control,基于角色的访问控制) 把权限归入角色——管理员、编辑、查看者——再把用户分配给角色。简单、易读,对大多数应用来说也够用:「编辑可以发布,查看者不行」。当规则取决于上下文或具体的那条记录时,它就显得粗糙了,这时就轮到后面的模型出场。

usersrolespermissionsAliceadmineverythingBobeditorpublish · editChenviewerread
用户 → 角色 → 权限集合。权限授予角色,而不是直接授予个人。
ModelRBAC· Role-Based Access Control
问题
给每个人单独授权,是一团乱麻。
思路
权限给角色;角色给用户。
例子
「编辑可以发布;查看者只能读。」
代价
当规则取决于上下文或具体记录时,过于粗糙。
何时使用
大多数应用。
关联
更细粒度——ABAC;永远要再加一个 Ownership Check。

ABAC——按策略授权

是一条规则,而不是一份名单——「经理可以在工作时间、用公司设备批准 1 万以下的报销」——每次都重新计算。

ABAC(Attribute-Based Access Control,基于属性的访问控制) 根据用户、资源和上下文的属性来决定——角色以及金额以及时间以及设备。比固定角色灵活得多,也复杂得多。该采用它的信号是:你在角色检查之上堆了不止几个自定义的 if 条件。

User attributesrole = managerResource attributesexpense: 8kContext14:00 · company laptopPolicyamount < 10k AND …Allow / Denyevaluated every time
用户、资源和上下文的属性进入一条策略,每个请求都重新做出决定。
ModelABAC· Attribute-Based Access Control
问题
权限不仅取决于角色,还取决于金额、时间、设备。
思路
一条针对用户、资源和上下文属性的策略规则。
例子
经理在工作时间、用工作笔记本批准 1 万以下的报销。
代价
编写、阅读和测试都难得多。
何时使用
当你的角色上已经粘了不止几个自定义 if 的时候。
关联
从 RBAC 长出来;立足于 Least Privilege。

Ownership Check——你能碰这条记录吗?

你有一张有效的图书证(一个角色),但这并不让你去读别人借走的那本书里潦草写下的笔记。

归属检查(ownership check,对象级授权) 是最常被漏掉的检查:即使是有效的「编辑」,也只应编辑他自己的文档,而不是所有人的。角色说「编辑可以编辑」;你仍然得验证这位编辑拥有这一行。忘了它,是最常见的访问控制 bug——改一下 URL 里的 id,你就进到了别人的数据里。永远检查对象,而不只是角色。

PUT /docs/42by BobRole checkeditor? ✓Owner checkdoc 42 owner = Bob?Allowonly if both passthe most-missed check
两道检查:角色(「是编辑吗?」)和归属(「这份文档是他的吗?」)。两者都通过才放行。
PracticeOwnership Check· 对象级 authZ
问题
角色允许「编辑文档」——结果用户编辑了别人的文档。
思路
除了角色,还要检查这个对象是否属于这个用户。
例子
在 PUT /docs/42 之前,检查文档 42 的所有者是不是当前用户。
代价
每次访问对象都多一道检查。
何时使用
只要数据有所有者,就总是用。
关联
补充 RBAC/ABAC;缺了它——就是 IDOR。

默认拒绝。只授最少。并且永远不只问「什么角色」,还要问「谁的记录」。

§ 06

认证以少数几种众所周知的方式失败——一遍又一遍,在一个又一个应用里,都是同样的那些。了解它们,就是防御的一半。

Broken Access Control / IDOR——忘了检查

一栋楼,前门有人把守,但里面每扇门都没锁——一旦进去,你可以去任何地方。

Broken Access Control(失效的访问控制) 是排名第一的网页漏洞:一个端点认证了用户,却忘了检查他是否被授权做他所请求的事。它最常见的形式是 IDOR(Insecure Direct Object Reference,不安全的直接对象引用)——把 /orders/123 改成 /orders/124,就看到别人的订单,因为服务器从没检查过归属。数百万条记录的泄露,追根溯源正是这个。在每个请求上、在服务端、针对实际的对象检查 authZ。

Boblogged inGET /orders/123Bob's order ✓GET /orders/124Ann's order!ServerauthN ✓ · owner check ✗fix: WHERE id = 124 AND owner = current_user
Bob 老老实实登录了,但服务器不检查归属——改一下 URL 里的 id,你就看到了别人的订单。
Anti-patternBroken Access Control / IDOR· 缺少权限检查
是什么
用户已通过认证,但没有检查他对该对象的权限。
表现
/orders/123 → /orders/124——别人的订单就出现了。
危害
数百万条记录的泄露。
修复

在每个请求上、在服务端做 authZ:WHERE id = 124 AND owner = current_user。

关联
用 Ownership Check 和 Least Privilege 来根治。

Plaintext / Fast Hash——把密码存错了

把整条街每户人家的备用钥匙,都放在门廊上的一个玻璃罐里。

把密码存成明文——或者用 MD5 这样的快速哈希——意味着一次数据库泄露就暴露所有人,而被复用的密码还会连累他们的其他账户。密码必须缓慢且逐个地做哈希(Argon2、bcrypt),这样即使被偷,破解起来也代价高昂。哈希慢是目的,不是缺陷。

MD5 / SHA-1fast hashCracked fastevery leaked user exposedArgon2 / bcryptslow + saltedToo costlyeven when stolenattacker guesses: cheapattacker guesses: expensive
快速哈希——猜一次很便宜,密码很快被破解。带盐的慢哈希——每一次猜测都很昂贵。
Anti-patternPlaintext / Fast Hash· 明文或 MD5 存密码
是什么
数据库里的密码是原样的,或只套了一个快速哈希。
危害
一次泄露就暴露所有密码;被复用的密码还会打开其他账户。
修复
带盐的慢哈希:Argon2 或 bcrypt。
关联
正确的版本是 Password Hashing。

Token Leakage——泄露并轻信令牌

把警报密码写在便利贴上贴在门上——锁没问题;是你自己把秘密发出去了。

令牌泄露(token leakage) 就是令牌和密钥逃逸出去的方式。它们通过 URL(会被记入日志)、浏览器的 localStorage(脚本能读)以及提交进 git 的秘密泄露。而且令牌必须被验证,而不是因为它出现了就信任它——被篡改或已过期的必须拒绝。把每一份凭证都当作总有一天会泄露的东西来对待:短有效期、轮换,绝不放在会被记录或被抓取的地方。

Token / API key?token= in URLends up in logslocalStorageread by XSSgit commitpublic foreverServer verifiessignature · expiry · scopeshort-lived · rotatable
三条经典的泄露路径:日志里的 URL、localStorage 和 git。防御:服务器验证每个令牌,有效期要短,密钥要轮换。
Anti-patternToken Leakage· 令牌和密钥泄露
是什么
凭证最终落在了可以被读到或被找到的地方。
表现
URL 里的 ?token=、localStorage 里的令牌、git 提交里的密钥。
危害
谁找到令牌,谁就能进来。
修复
验证签名和过期时间;短有效期、轮换;浏览器里用 httpOnly Cookie。
关联
对 JWT 和 API Key 伤害最大。

Trusting the Client——相信浏览器说的话

一家店让顾客自己写价签,在收银台也从不核对。

这就是轻信客户端这个反模式。客户端发来的任何东西——表单里的隐藏字段、cookie 里的角色、一个 isAdmin=false 标志——都可以被伪造。 浏览器、应用、请求,全都在用户的控制之下。每一道真正的检查都必须发生在服务器上,那是用户改不了的地方。客户端只是建议;服务器才是权威。

CLIENT · user-controlledhidden form fieldcookie: role=adminisAdmin=falseServerre-checks everything itselfsuggestionthe authority
客户端的一切,用户都能改。只有服务器做出决定。
Anti-patternTrusting the Client· 客户端侧的检查
是什么
依据客户端发来的数据做出权限决定。
表现
cookie 里的角色、隐藏字段、请求里的 isAdmin 标志。
危害
用户自己给自己授权。
修复
每道检查都在服务器上;客户端只是建议。
关联
Broken Access Control 的常见成因。

几乎每一起认证泄露都是下面之一:一道被跳过的检查、一个存错的秘密,或者被轻信的客户端。

§ 07

好的认证,主要在于正确地使用经过验证的工具、毫不松懈地检查权限——而不是耍聪明。下面是怎么选。

Auth Provider——用库或服务商

你不会自己制造安全带——你会装上经过认证的,由专家设计、经过多年碰撞测试的那种。

对几乎所有人来说,正确的做法是不要自己搭认证: 用你框架自带的认证,或者一个 auth provider(Auth0、Clerk、Supabase、Cognito 之类),它会处理哈希、会话、MFA、OAuth,以及你想都想不到的边界情况。你的工作是把它正确接入,并掌管授权——只有你自己才懂的那部分。

Auth library / providerPassword hashingSessions · tokensMFA · passkeysOAuth · OIDCYour appowns AuthZ: the rulesidentity
服务商负责身份:哈希、会话、MFA、OAuth。你的应用掌管访问规则。
ToolAuth Provider· 认证库或服务
问题
手工搭的认证几乎总会留下漏洞。
思路
把 authN 交给经过验证的工具;authZ 留给自己。
例子
Auth0、Clerk、Supabase、Cognito,或你框架内置的认证。
代价
依赖一个供应商;必须正确接入。
何时使用
几乎总是。
关联
是 Don't Roll Your Own Auth 的落地。

Match the mechanism to the app——让机制匹配应用

家门钥匙、车钥匙和酒店房卡都是「钥匙」——你按门来选,而不是按时髦与否。

普通网页应用用会话(简单、可即时撤销);需要跨服务的无状态扩展时,用短有效期 JWT 加 refresh;社交登录和 SSO 用 OIDC;机器对机器用 API key。凡是重要的地方都加上 MFA,尤其是管理员账户。按你应用的形态来选,而不是按听起来高级与否。

Normal web appSessionsMany services / APIsShort JWT + refreshSocial login / SSOOIDCMachine to machineAPI keyAdmin / high risk+ MFA
需求在左,机制在右。按应用的形态来选。
上线认证之前
  • 密码是否做了慢哈希(Argon2/bcrypt),绝不存成文本?
  • 是否提供了 MFA,并且对管理员强制启用?
  • 每个端点是否都在检查 authZ——既查角色也查对象的归属?
  • 令牌是否在 httpOnly cookie 里、有效期短,并且绝不在 URL 或 git 里?
  • 真正的检查是否在服务器上,绝不信任客户端发来的角色?
  • 我用的是经过验证的库/服务商,而不是手搓的方案吗?
说明它已经坏了的气味
  • 改一下 URL 里的 id 就显示别人的数据。
  • 权限存在 cookie 或隐藏字段里,客户端可以编辑。
  • 密码存成明文或 MD5。
  • 除了等它过期,没有办法撤销一个已登录的用户。
  • 能造成最大破坏的账户没有 MFA。
  • 你自己写了令牌或加密方案。
说明你做对了的迹象
  • authN 和 authZ 是分开的,并且 authZ 在每个请求上都检查。
  • 每次访问对象都会验证这个用户拥有这个东西。
  • 凭证是经过哈希、限定范围、短有效期、可轮换的。
  • 服务器是权威;没有任何东西信任客户端。
  • 你能撤销访问,并在日志里看到谁做了什么。
  • 繁重的工作由经过验证的库或服务商承担,而不是你自己的代码。

这一切如何串起来

酒店住客的路径:前台出示护照,手里拿着房卡,而每扇门自己决定要不要打开。

登录因素(密码、MFA、passkey、OIDC、API key)通向 authN。它的结果随每个请求一起,放在会话或 JWT 里传递,最好存在 httpOnly cookie 中。在每个请求上,authZ 按角色或策略做决定——并且总是检查对象的归属。

Password + hashMFAPasskeyOAuth2 / OIDCAPI keyAuthNwho?SessionJWThttpOnly cookiecarries itAuthZmay?every requestRBAC / ABACOwnership
从左到右:因素 → authN → 会话或 JWT → 每个请求上的 authZ → 角色/策略 + 归属。

速查表:全部 22 个概念

两个问题

Authentication (authN)证明身份

证明你是谁。通常每次登录一次。

Authorization (authZ)决定权限

决定你能做什么。在每个请求上。

Don't Roll Your Own Auth标准重于自造

用经过验证的标准和库,而不是自己的方案。

证明你是谁

Password Hashing加盐的慢哈希

存带盐的慢哈希(Argon2、bcrypt),绝不存密码本身。

MFA多因素, 2FA

第二因素:你拥有的或你本身的。最便宜的大收益。

PasskeysWebAuthn

设备上的一对密钥,用签名登录。没有什么可钓鱼的。

保持登录状态

Stateless HTTP请求之间没有记忆

服务器忘掉请求——每个请求都需要一张通行证。

Session服务端会话

cookie 里是 id;真相在服务器上。即时撤销。默认选择。

JWTJSON Web Token

内含 claims 的签名令牌。不用查询,但难以撤销。

httpOnly CookieJS 读不到的 cookie

浏览器令牌该放的地方。XSS 读不到它。

委托访问

OAuth 2.0委托授权

用范围很窄的令牌访问某人的账户,不要密码。

OIDCOpenID Connect

OAuth2 + ID token:「这是他们本人」。社交登录和 SSO。

API Key机器凭证

给程序用的秘密。窄范围、轮换,绝不进 git 或 URL。

权限

Least Privilege默认拒绝

默认「不」;只授予所需的最低限度。

RBACRole-Based Access Control

按角色授权:管理员、编辑、查看者。

ABACAttribute-Based Access Control

按针对用户、资源和上下文属性的策略授权。

Ownership Check对象级 authZ

不只问「什么角色」,还要问「谁的记录」。

它怎么崩坏

Broken Access Control / IDOR缺少权限检查

检查被跳过了:改 URL 里的 id,就看到别人的数据。

Plaintext / Fast Hash明文或 MD5 存密码

一次数据库泄露——所有密码全部外泄。

Token Leakage令牌泄露

令牌在 URL、localStorage、git 里。谁找到一个,谁就能进来。

Trusting the Client客户端侧的检查

客户端只是建议;服务器才是权威。

把它做对

Auth Provider认证库或服务

把 authN 交给经过验证的工具;authZ 留给自己。

认证不是耍聪明的地方。它是正确使用标准、默认拒绝、并且永远、永远不信任客户端的地方。

速成课结束 · 7 章 · 22 个概念

接下来是深入:OWASP Top 10 及其速查表、OAuth 2.0 与 OpenID Connect 规范,以及你所用 auth provider 自己的指南。但请把这两个问题带到任何地方——你是谁,又被允许做什么?第一个,交给经过验证的工具来回答;第二个,在你的服务器上,每一次都回答。