
本文详解如何在 Laravel 9 中绕过 Passport 默认的授权页面,通过自定义 API 接口(如 /oauth/authorize)实现 SPA 场景下的无界面授权流程,支持客户端动态提交 client_id、scopes 等参数并返回授权码(authorization code)。
本文详解如何在 laravel 9 中绕过 passport 默认的授权页面,通过自定义 api 接口(如 `/oauth/authorize`)实现 spa 场景下的无界面授权流程,支持客户端动态提交 `client_id`、`scopes` 等参数并返回授权码(authorization code)。
在使用 Laravel Passport 构建 OAuth2 授权服务时,其默认行为是通过 GET /oauth/authorize 路由渲染 Blade 模板(如 resources/views/vendor/passport/authorize.blade.php),引导用户点击“Approve”或“Deny”。然而,在现代 SPA(如 Vue/React 前端)或第三方集成场景中,这种跳转+模板渲染模式不可控且难以嵌入。此时,你需要一个纯 JSON API 来接收授权请求、校验用户会话与权限,并直接返回 code 或错误响应。
✅ 正确做法:重写授权路由 + 自定义控制器
Passport 的授权逻辑由 Laravel\Passport\Http\Controllers\AuthorizationController 处理,默认绑定在 Passport::routes() 中。你不能直接修改该类,但可通过 Laravel 路由优先级机制覆盖默认路由:
- 在 app/Providers/AuthServiceProvider.php 的 boot() 方法中,先调用 Passport::routes(),再注册自定义路由(确保你的路由优先匹配):
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CustomAuthorizationController;
public function boot()
{
$this->registerPolicies();
Passport::routes(); // 注册默认 passport 路由(含 /oauth/authorize)
// ⚠️ 关键:覆盖 GET /oauth/authorize(注意是 GET,非 POST)
Route::get('/oauth/authorize', [
'uses' => CustomAuthorizationController::class . '@authorize',
'as' => 'passport.authorize',
]);
// 可选:覆盖 POST /oauth/authorize(处理用户确认动作)
Route::post('/oauth/authorize', [
'uses' => CustomAuthorizationController::class . '@store',
'as' => 'passport.authorize.store',
]);
}? 注意:/oauth/authorize 是标准 OAuth2 授权端点,必须为 GET(用于展示授权页/返回 code)和 POST(用于提交同意/拒绝)。不要误用 POST 替代 GET。
- 创建自定义控制器(如 app/Http/Controllers/CustomAuthorizationController.php):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Passport\Http\Controllers\AuthorizationController as BaseAuthorizationController;
use Laravel\Passport\TokenRepository;
use Laravel\Passport\ClientRepository;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class CustomAuthorizationController extends Controller
{
protected $tokenRepository;
protected $clientRepository;
public function __construct(TokenRepository $tokenRepository, ClientRepository $clientRepository)
{
$this->tokenRepository = $tokenRepository;
$this->clientRepository = $clientRepository;
}
// GET /oauth/authorize —— 返回授权决策结果(JSON)
public function authorize(Request $request)
{
// 1. 校验必需参数
$validated = $request->validate([
'client_id' => 'required|exists:oauth_clients,id',
'redirect_uri' => 'required|url',
'response_type' => 'required|in:code',
'scope' => 'nullable|string',
'state' => 'nullable|string',
]);
// 2. 检查用户是否已登录(SPA 应提前完成登录并保持 session)
if (!Auth::check()) {
return response()->json([
'error' => 'unauthorized',
'message' => 'User must be authenticated.'
], 401);
}
$user = Auth::user();
$client = $this->clientRepository->find($validated['client_id']);
// 3. (可选)检查 redirect_uri 是否匹配客户端白名单
if (!in_array($validated['redirect_uri'], json_decode($client->redirect, true))) {
throw ValidationException::withMessages(['redirect_uri' => 'Invalid redirect URI.']);
}
// 4. 生成 authorization code(复用 Passport 内部逻辑)
$code = $this->tokenRepository->createAuthCode(
$client->id,
$user->id,
$validated['redirect_uri'],
$validated['scope'] ?? ''
);
// 5. 重定向回第三方 client(标准 OAuth2 流程)
$redirectUrl = $validated['redirect_uri'] . '?' . http_build_query([
'code' => $code->code,
'state' => $validated['state'] ?? null,
]);
return response()->json([
'redirect_url' => $redirectUrl,
'code' => $code->code, // 或仅返回 code,由前端自行跳转
]);
}
// POST /oauth/authorize —— 处理用户显式同意/拒绝(SPA 提交表单时调用)
public function store(Request $request)
{
$request->validate([
'client_id' => 'required|exists:oauth_clients,id',
'redirect_uri' => 'required|url',
'state' => 'nullable|string',
'approve' => 'required|boolean', // true=allow, false=deny
]);
if (!Auth::check()) {
return response()->json(['error' => 'unauthorized'], 401);
}
if ($request->boolean('approve')) {
// 生成 code 并重定向(同上)
$code = $this->tokenRepository->createAuthCode(
$request->client_id,
Auth::id(),
$request->redirect_uri,
$request->scope ?? ''
);
$redirectUrl = $request->redirect_uri . '?' . http_build_query([
'code' => $code->code,
'state' => $request->state,
]);
return response()->json(['redirect_url' => $redirectUrl]);
} else {
// 拒绝:重定向并携带 error
$redirectUrl = $request->redirect_uri . '?' . http_build_query([
'error' => 'access_denied',
'state' => $request->state,
]);
return response()->json(['redirect_url' => $redirectUrl]);
}
}
}⚠️ 重要注意事项
- Session 与 CSRF:确保 SPA 与 Laravel 后端共享同一域名(或正确配置 CORS + SameSite cookie),以便 Auth::check() 有效;若跨域,需启用 withCredentials 并配置 SESSION_DOMAIN。
- 不要删除默认视图:即使不使用 Blade 模板,也建议运行 php artisan vendor:publish --tag=passport-views 并保留原始视图作为兜底(防止意外触发)。
- 安全加固:生产环境务必验证 redirect_uri 严格匹配客户端注册值(如 json_decode($client->redirect, true)),防止开放重定向漏洞。
- 缓存清理:修改路由后,执行 php artisan route:clear,避免旧路由缓存干扰。
- OAuth2 规范遵循:此方案仍遵守 RFC 6749,返回 code 后,第三方客户端需用该 code 向 /oauth/token 兑换 access token(此步无需改动,Passport 默认支持)。
通过以上方式,你即可完全掌控授权流程——前端 SPA 自行渲染授权 UI,调用你的 /oauth/authorize API 完成用户决策,并无缝对接标准 OAuth2 流程,兼顾安全性与灵活性。


















