PHP OAuth 2.0 服务端必须用 league/oauth2-server 或 Laravel Passport,强制实现5个核心存储接口;state 必须生成、校验且立即销毁;令牌请求需严格满足URL、方法、Content-Type、Authorization头及redirect_uri一致性;refresh_token单次有效,刷新时须用 grant_type=refresh_token 并更新存储。

PHP 本身不内置 OAuth 2.0 服务端实现,直接用 $_GET['code'] + 手写 cURL 调用令牌接口,大概率会踩签名错、重定向 URI 不匹配、state 校验遗漏、刷新令牌失效等坑——别自己造轮子,用成熟库是底线。
用 league/oauth2-server 搭建授权服务器(Laravel 或纯 PHP)
这是目前最稳定、文档最全的 PHP OAuth 2.0 服务端库,支持所有标准 grant 类型(authorization_code、client_credentials、password、refresh_token),且强制要求显式实现存储层接口,倒逼你理清数据流向。
关键点:
- 必须实现
ClientRepositoryInterface、AccessTokenRepositoryInterface等 5 个核心接口,不能跳过——否则连AuthorizationServer实例都构造不出来 -
authorization_code流程中,AuthCodeRepositoryInterface::getNewToken()返回的 code 必须带过期时间(默认 10 分钟),且只能用一次 - Laravel 用户优先选
laravel/passport(底层就是封装了league/oauth2-server),它自动注册路由、迁移表、提供Passport::tokensExpireIn()配置项 - 纯 PHP 项目别直接 require
league/oauth2-server后就写路由——先跑通它的 examples 目录里的auth-server.php
state 参数不是可选字段,而是 CSRF 防护刚需
几乎所有手写 OAuth 2.0 客户端的 PHP 代码,都在这里翻车:只生成 state 存 session,但没验证回调时的 $_GET['state'] 是否与 session 中一致,或验证后没销毁 session 中的值。
立即学习“PHP免费学习笔记(深入)”;
正确做法:
- 发起授权请求前,生成随机字符串(如
bin2hex(random_bytes(16))),存入$_SESSION['oauth2_state'] - 构造授权 URL 时,必须带上
&state=xxx参数(例如:https://auth.example.com/authorize?response_type=code&client_id=abc&state=def123...) - 用户回调后,立刻比对
$_GET['state'] === $_SESSION['oauth2_state'],通过则 立即 unset($_SESSION['oauth2_state']) - 漏掉 unset 会导致同一个
state被重复使用,攻击者可截获 code 并重放
获取 access_token 时,grant_type=authorization_code 的 POST 请求必须满足 3 个硬性条件
哪怕参数名拼错一个字母、Content-Type 少写、Basic Auth 编码错,都会返回 401 或 400,错误信息还常是模糊的 "invalid_request"。
检查清单:
- 请求 URL 是令牌端点(如
https://auth.example.com/token),不是授权端点 - HTTP 方法必须是
POST,且Content-Type: application/x-www-form-urlencoded - 请求体必须是 form-encoded,不是 JSON:
code=xxx&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback - 必须在
AuthorizationHeader 中传 client credentials:Authorization: Basic <base64(client_id:client_secret)>;注意不是传client_id和client_secret在 body 里(那是非标准写法,部分服务容忍,但 RFC 不允许) -
redirect_uri必须和申请 code 时用的完全一致(协议、域名、路径、查询参数都要相同),否则报"invalid_grant"
access_token 刷新失败?大概率是没保存 refresh_token 或用了已失效的旧值
refresh_token 只在首次用 authorization_code 换 access_token 时下发一次,之后每次刷新都得用它——但它不是永久有效的。
常见疏漏:
- 首次响应里确实有
refresh_token字段,但代码只取了access_token,没存refresh_token到数据库或加密 Cookie - 刷新请求发出去后返回
"invalid_grant",其实是上一次刷新成功后,原refresh_token已被服务端作废(多数实现遵循“单次使用即失效”) - 刷新时仍传
grant_type=authorization_code(错!该传grant_type=refresh_token) - 没在刷新请求里带上
refresh_token=xxx参数,或拼写成refresh_token_value等非标准字段名
真正麻烦的是:不同授权服务器对 refresh_token 生命周期策略差异极大——有的 7 天过期,有的绑定设备指纹,有的甚至不发。别假设它能用一年,每次刷新成功后,务必用新值覆盖旧值。



















