不能手写 authorization_code 流程,因其需严格实现5个存储接口并满足状态约束与时效校验:authcoderepositoryinterface须返回带expiresin和isrevoked的授权码;redirecturirepositoryinterface须全字符串比对;clientrepositoryinterface须校验redirecturis数组;code生成须用cryptographically secure随机数;换token后必须立即作废code。

直接用 league/oauth2-server,别手写授权服务器。自己拼 SQL 存 code、手动验 redirect_uri、漏掉 AuthCodeRepositoryInterface::getNewToken() 的过期时间控制,90% 的线上 token 无效或重放漏洞都出在这几处。
为什么不能手写 authorization_code 流程
授权码模式不是“收到 code 就调 /token 接口”这么简单。它要求服务端严格实现 5 个存储接口,且每个环节都有状态约束和时效性校验:
-
AuthCodeRepositoryInterface必须返回带expiresIn(默认 10 分钟)和isRevoked状态的授权码,否则攻击者可截获 code 后反复使用 -
RedirectUriRepositoryInterface必须做**全字符串比对**:https://a.com/callback≠https://a.com/callback/,大小写、协议、端口、末尾斜杠缺一不可 -
ClientRepositoryInterface返回的 client 必须含redirectUris数组(不是单个字符串),且校验时要遍历匹配,不能只取第一个 - 生成 code 时必须调用
bin2hex(random_bytes(32)),不能用md5(time().rand())这类可预测值 - 换 token 成功后,必须立刻调用
$authCodeRepository->revokeAuthCode($code),否则该 code 可二次使用
league/oauth2-server 初始化必须填满的 5 个接口
构造 AuthorizationServer 实例前,你得先实现并传入这 5 个仓库对象,少一个就会 fatal error:
-
ClientRepositoryInterface:查 client_id 是否存在、是否启用、是否匹配 redirect_uri -
AccessTokenRepositoryInterface:存 access_token(含 user_id、client_id、scopes、expires_at)、查 token 是否有效 -
ScopeRepositoryInterface:校验请求的scope是否在 client 白名单内(如只允许read:profile,却传了read:profile write:email就该拒) -
AuthCodeRepositoryInterface:存、查、作废授权码;getNewToken()必须返回['id' => 'xxx', 'expiresIn' => 600, 'redirectUri' => '...'] -
RefreshTokenRepositoryInterface:存 refresh_token(注意:它和 access_token 是分开的,且必须单次有效——用完即删)
纯 PHP 项目别跳过 examples/auth-server.php 直接写路由,那个文件里已强制演示了全部接口如何串联。
回调换 token 时 POST 请求的三个硬性头与体
用户从授权页跳回你的 /callback 后,你用 code 换 token 的 POST 请求,以下三点必须同时满足,否则返回模糊的 invalid_request 或 invalid_client:
- 请求头
Content-Type: application/x-www-form-urlencoded(不是application/json,也不是空) - 请求头
Authorization: Basic {base64(client_id:client_secret)}(冒号不能漏,client_id和client_secret之间必须有英文冒号) - POST body 必须用
http_build_query()拼装,含grant_type=authorization_code、code、redirect_uri(值要和授权请求时**完全一致**,包括协议、端口、路径、末尾斜杠)
微信等平台还额外要求 body 含 client_id 和 client_secret ——这不是标准 OAuth 2.0 行为,但你得适配,否则拿不到 token。
state 参数不是客户端的事,服务端也得管
很多人以为 state 只是客户端防 CSRF 的事,其实服务端在生成授权 URL 前就得参与:
- 服务端生成
state时,必须立即存入$_SESSION['oauth2_state'],且不能只存字符串,要连同当前时间戳、client_id 一起序列化,防止跨 client 重放 - 授权请求到达
/authorize端点时,服务端要检查$_GET['state']是否存在于 session 中,且未超时(建议 5 分钟) - 用户同意后,服务端返回 302 重定向时,必须把原始
state原样带回 redirect_uri,不能重新生成 - 回调页收到
state后,比对通过就立刻unset($_SESSION['oauth2_state']),不 unset = 凭证复用 = CSRF 可重放
最易被忽略的是:Laravel Passport 默认关掉了 session-based state 校验,你要手动在 config/auth.php 里开 'stateful' => ['your-domain.com'],否则本地开发时 localhost 和生产域名 session 不通,state 总是不匹配。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











