那天下午两点多,客服转来一条消息:有个用户说他的账号昨晚还握在自己手机里,后台却多了一条他没发过的动态。更麻烦的是,这个账号在管理列表里还挂着当时的登录态。我第一反应是数据库被拖了库,可翻完审计日志和登录日志,没有任何异常操作。真正的问题在我自己写的第三方登录回调里OAuth2 授权码模式,我漏了 state 校验,也没上 PKCE。
第三方登录这套东西要跑起来,前提是有一台稳定、能扛住回调接口被刷的机器;如果你正在处理这类问题,可以先看看这几家的报价和技术说明:raksmart(全球云服务器,机房侧提供 1Tbps+ 的 DDoS 防御)、zorocloud(原生 IP 精品线路,多地机房接驳高端线路)
一、先把授权码模式走一遍,问题出在哪才清楚
我自己手写的流程只有四步。第一步,前端点「用第三方账号登录」,后端拼一个授权地址跳过去,参数是 response_type=code、client_id、redirect_uri、scope,外加一个随机串 state。第二步,用户在第三方那边点了确认,第三方带着 code 和 state 回调我的 redirect_uri。第三步,我拿 code 去第三方换 access_token。第四步,本地建会话,把用户丢进首页。
这四步本身没错,错的是我在第二和第三步上省了东西:state 我是直接从请求参数里透传再透传的,没跟服务端会话做绑定;换 token 的时候我只用了 client_secret,没带 code_verifier;code 也没落库,消费完没标记。

二、坑一:state 不校验,登录态能被「套娃」
state 的作用只有一个:把这次授权请求和发起它的那次浏览器会话绑在一起。正确做法是跳转前生成一串随机值,写进服务端 session,回调时原样比一次,不一致直接拒绝。我当时图省事,只在 URL 里带着它,没存也没比。
攻击者只要拿到自己账号的一个 code,拼成回调地址发给受害者,受害者一点,本地会话里登进去的就是攻击者的账号;反过来同理,受害者自己的授权码可以被塞进攻击者的浏览器,登录态直接被套走,这就是所谓的登录 CSRF。
我是在回调接口的访问日志里看出来的:半小时内 /oauth/callback 的请求量涨了三倍多,参数组合高度雷同,只是 code 和 state 在换。这批请求单独看都不算「异常」,因为它们格式完全合法。

三、坑二:没上 PKCE,授权码一泄露就能被重放
授权码理论上只能用一次,服务端换完 token 就作废。但只要我这边不落库、不做一次性消费,攻击者拿到 code 后可以反复来换,第三方那边如果也没做强校验,等于一个 code 换出一堆 token。更关键的是 code 是通过浏览器地址栏明文传的,在公共 WiFi、公司代理、恶意 App 劫持 scheme 的场景下,它随时可能被人抄走。
PKCE(Proof Key for Code Exchange)就是补这个洞的。跳转前客户端生成一个 code_verifier,存本地;跳转时带上 code_challenge,值是 BASE64URL(SHA256(code_verifier));换 token 时再把原始的 code_verifier 交上去,服务端算一遍哈希比对。攻击者就算截到 code,没有 code_verifier 也换不出 token。
对纯后端服务来说,可以把 code_verifier 用 PKCE 模式生成后也存在服务端 session 里,效果一样。这部分改动量很小,一个随机串加一次哈希比对而已。
四、坑三和坑四:redirect_uri 白名单、code 一次性
redirect_uri 我最初只做了前缀判断,攻击者用一个长得像的域名就能把 code 引到自己服务器上去。后来改成严格等值匹配,只认配置里那几条完整地址,一条都不多。
code 消费那一步,我改成落库:拿到 code 先查库,存在且未使用、未过期才继续用,用完立刻打上已消费的标记,并且设置 5 分钟过期。第三方那边换 token 失败时会返回 invalid_grant,本地再兜一层同样的判断,防止它因为网络抖动重试时把 code 用掉两次。
五、修完之后,我是怎么验证的
光看代码不放心,我用几条 curl 命令模拟了攻击路径。第一条,把回调里的 state 换成随便一串,服务端应该直接 400 拒绝,日志里应该出现 state mismatch。第二条,把同一个 code 连着换两次 token,第一次成功,第二次必须是 invalid_grant。第三条,把 redirect_uri 改成别的域名,应该在跳转前就被挡掉,根本不会发出请求。第四条,用同一个 state 并发发两次请求,只应该有一次能走到换 token 那一步。
另外我在回调接口前面加了限流,回调是公开暴露的接口,很容易被人拿来当压测靶子;登录成功的会话 Cookie 也加上了 SameSite 属性。这一轮下来,那个用户账号再没出现过异常。

六、服务器侧也别忘了留一手
这次故障里最有用的其实不是代码,而是日志。我把回调接口的请求参数、来源 IP、命中了哪条校验规则全部落盘,并保留了 30 天,那半小时的异常请求才还原得出来。日志里最怕的是被刷爆,所以机器上要做日志切割和磁盘水位监控,磁盘一满,攻击和告警会同时消失。
还有一点:这类接口放在小配置机器上,被人一轮压测就可能把 CPU 和带宽吃满,回调超时之后授权流程直接不可用。真要跑长期的第三方登录服务,机器的带宽余量、防御能力和多线接入都要提前算进去,别等被打的时候再临时换。

回头看,这 30 分钟里真正被我漏掉的就两个词:state 和 PKCE。它们分别补上了「跨会话套娃」和「授权码被抄走重放」两个洞,成本不过几十行代码。第三方登录看着是别人家的账号密码,但授权码在你服务器和浏览器之间来回跑的那几秒钟,它就是你的责任,校验必须自己扛。


喜欢
顶
难过
囧
围观
无聊



