Laravel Passport PKCE模式下注册后重定向SPA并授权的实现问题
问题背景与需求
- 架构:认证/资源服务器与单页应用(SPA)部署在不同容器、服务器上,采用Proof Key for Code Exchange(PKCE)公开客户端
- 登录流程:遵循PKCE规范,运行正常
- 注册流程:用户在SPA选择定价套餐后,携带套餐、权限范围参数跳转到认证服务器完成注册与支付,需重定向回SPA仪表盘并携带有效访问凭证,实现用户直接使用应用
- 核心问题:PKCE公开客户端无法通过
HasApiToken的createToken方法生成令牌(无个人/私有客户端),不想通过服务端内部调用OAuth端点(非最佳实践),需合规实现跨域认证与注册后的凭证传递
可行解决方案
方案1:复用PKCE授权码流程(推荐)
利用已有的PKCE授权码机制,在注册完成后直接生成授权码,让SPA按正常流程换取令牌:
- 在认证服务器的注册完成逻辑中,生成PKCE兼容的授权码:
use Laravel\Passport\Client; use Laravel\Passport\Code; use Illuminate\Support\Str; // 获取对应SPA的PKCE公开客户端 $spaClient = Client::where('name', '你的SPA客户端名称') ->where('personal_access_client', false) ->first(); // 生成关联当前用户的授权码 $authCode = Code::create([ 'user_id' => $user->id, 'client_id' => $spaClient->id, 'scopes' => request()->get('scopes', []), // 注册时携带的权限范围 'redirect_uri' => $spaClient->redirect, 'code' => Str::random(40), 'expires_at' => now()->addMinutes(5), ]); - 重定向回SPA,携带授权码与state参数(保持PKCE流程一致性):
return redirect()->to(sprintf( '%s?code=%s&state=%s', $spaClient->redirect, $authCode->code, request()->get('state', Str::random(20)) )); - SPA收到授权码后,用之前生成的
code_verifier向认证服务器请求access_token,完成正常PKCE流程
方案2:创建专用内部客户端(多应用SSO场景适配)
为SSO体系创建一个仅用于注册回调的私有客户端,合规生成令牌:
- 生成私有客户端:
记录客户端ID与密钥,存入环境变量php artisan passport:client --private - 在注册控制器中,通过该客户端为用户生成令牌:
use Laravel\Passport\Client; use Illuminate\Support\Str; $internalClient = Client::find(env('REGISTRATION_CALLBACK_CLIENT_ID')); $plainTextToken = Str::random(80); // 直接生成关联客户端的令牌 $user->tokens()->create([ 'name' => 'Registration Session', 'token' => hash('sha256', $plainTextToken), 'abilities' => request()->get('scopes'), 'client_id' => $internalClient->id, 'expires_at' => now()->addDays(1), ]); - 重定向回SPA时,将令牌放在URL哈希片段中传递(避免日志泄露):
SPA通过return redirect()->to(sprintf( '%s#access_token=%s&token_type=Bearer&expires_in=86400', $spaClient->redirect, $plainTextToken ));window.location.hash提取令牌,存入localStorage或sessionStorage
方案3:一次性临时凭证交换机制
生成一次性临时码,让SPA主动交换令牌,避免直接传递敏感凭证:
- 注册完成后生成临时码并缓存(设置短过期时间):
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; $tempCode = Str::random(60); // 5分钟过期,绑定用户ID Cache::put('reg_temp_code:' . $tempCode, $user->id, 300); - 重定向回SPA携带临时码:
return redirect()->to(sprintf( '%s?temp_code=%s', $spaClient->redirect, $tempCode )); - SPA向认证服务器的专用端点请求交换令牌:
// SPA端示例(axios) const tempCode = new URLSearchParams(window.location.search).get('temp_code'); axios.post('/api/reg/exchange-token', { temp_code: tempCode, client_id: '你的SPA客户端ID', scopes: ['read', 'write'] }).then(res => { localStorage.setItem('access_token', res.data.access_token); // 跳转至仪表盘 }); - 认证服务器验证临时码并返回令牌:
public function exchangeToken(Request $request) { $userId = Cache::pull('reg_temp_code:' . $request->temp_code); if (!$userId) { return response()->json(['error' => '无效或过期的临时码'], 400); } $user = User::findOrFail($userId); $client = Client::findOrFail($request->client_id); // 生成PKCE兼容的令牌 $token = $user->createToken('SPA Session', $request->scopes, $client)->accessToken; return response()->json([ 'access_token' => $token, 'token_type' => 'Bearer', 'expires_in' => 86400 ]); }
关键安全注意事项
- 禁止在URL查询参数中传递access_token:会被浏览器历史、服务器日志记录,存在泄露风险,优先用哈希片段或API交换
- 所有通信必须使用HTTPS,避免明文传输敏感数据
- 多应用SSO场景下,严格校验客户端的
redirect_uri,防止恶意跳转 - PKCE流程中,
code_verifier必须由SPA生成并存储,即使服务端生成授权码,后续令牌交换仍需验证该值
内容的提问来源于stack exchange,提问作者Kazeka SDL
相关产品推荐
相关产品推荐

