Laravel¶
本指南将向您展示如何使用 Authany 作为 OIDC 提供方,为 Laravel 应用添加用户身份验证。
Authany 支持多种登录方式,例如无密码登录、手机 OTP 和双重验证。本文将向您展示如何在 Laravel 应用中启用这些选项,而无需关心底层逻辑。
本指南适用于 Laravel 12 和 PHP 8.2+。
您将学到什么¶
- 如何创建 Authany 应用。
- 如何从 Authany 请求 OAuth 2.0 授权码。
- 如何使用 OAuth 2.0 访问令牌从 Authany 获取用户信息。
- 如何用 Authany 替换 Breeze 的本地密码登录,并通过用户的 Authany 主体标识(
sub)关联用户。
前提条件¶
要跟随本示例操作,您需要准备好以下条件:
- 一个免费的 Authany 账号。如果您还没有账号,请先注册。
- PHP 8.2 或更高版本、Composer 2,以及 Node.js 18 或更高版本。
您也可以从 Laravel 示例 GitHub 仓库克隆完整的示例应用来跟随操作。
我们将构建什么¶
我们要构建的示例应用使用 Laravel Breeze 搭建 UI 脚手架,包括 Blade 布局、仪表盘和个人资料页面。Authany 是唯一的身份提供方。我们会移除 Breeze 自带的本地注册、登录、密码重置和邮箱验证流程,让用户只能通过 Authany 登录。
把身份验证交给 Authany 后,您无需自己编写或维护相关逻辑,就能获得无密码登录、手机 OTP、双重验证等能力。

如何以 Authany 作为 OAuth 提供方,为 Laravel 添加用户身份验证¶
本节将带您完整走一遍构建示例应用的步骤。
步骤 1:配置 Authany 应用¶
在将 Authany 用作 OAuth 身份提供方之前,需要先在 Authany 管理台中设置一个应用。
登录 Authany 并选择一个项目,进入项目的 Applications(应用)部分。创建一个新应用,或配置一个已有应用,并将应用类型(Application Type)设置为 OIDC Client Application,如下图所示:

点击 Save(保存)进入应用配置页面。该页面会显示应用凭据和 OAuth 2.0 端点。

请记下 Client ID、Client Secret 和各个端点,稍后会在 Laravel 项目中用到。
步骤 2:添加重定向 URI¶
在应用配置页面上,向下滚动到 URL 部分,点击 Add URI(添加 URI)。如果您将在本地运行 Laravel 应用,请在文本框中输入 localhost:8000/oauth/callback,然后点击 Save(保存)。
授权完成后,Authany 会将用户重定向到该 URI,因此它必须指向 Laravel 应用中一个有效的路由。
步骤 3:创建 Laravel 项目¶
运行以下命令创建一个新的 Laravel 项目,该命令会安装 Laravel 12:
在编辑器中打开项目文件夹,删除 resources/views/welcome.blade.php,然后创建一个新的 resources/views/index.blade.php,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authgear + Laravel Demo</title>
</head>
<body>
<h1>Authgear + Laravel demo</h1>
<p>This demo shows adding user authentication to a Laravel app with Authgear (OIDC / OAuth 2.0).</p>
@if ($errors->any())
<p style="color: #b00020;">{{ $errors->first() }}</p>
@endif
<p><a href="{{ route('login') }}">Login with Authgear</a></p>
</body>
</html>
然后将根路由指向这个视图。打开 routes/web.php 并设置:
运行 php artisan serve,然后在浏览器中打开 localhost:8000,您应该会看到带有登录链接的着陆页。

步骤 4:安装 Laravel Breeze¶
Breeze 是 Laravel 的官方脚手架工具包。这里我们仅用它来搭建 UI(Blade 布局、仪表盘和个人资料页面),身份验证则交给 Authany 处理。
安装 Breeze:
运行安装程序来生成脚手架资源:
在安装过程中,选择 blade 作为技术栈,其他选项保持默认即可。
接下来,在 users 表中添加一个 oauth_uid 字段。该字段用于保存用户登录成功后从 Authany 获取的唯一 ID。
在 Laravel 12 中,users 表定义在 database/migrations/0001_01_01_000000_create_users_table.php 中(这一个文件同时创建了 sessions 表和缓存表,不再有单独按日期命名的 users 迁移文件)。请在 Schema::create('users', ...) 代码块中添加以下这一行:
然后在 app/Models/User.php 的 $fillable 数组中加入 oauth_uid,使该字段可以被批量赋值:
本示例使用 SQLite,因此不需要数据库服务器。全新的 Laravel 项目在 .env 中默认已经配置了 DB_CONNECTION=sqlite。创建数据库文件并运行迁移:
步骤 5:添加 Authany 配置¶
请将 Authany 的相关设置保存在专门的配置文件中,而不要在控制器里直接调用 env()。这样才能让 php artisan config:cache 在生产环境中正常工作:一旦配置被缓存,env() 在配置文件之外就会返回 null。
创建 config/authgear.php,内容如下:
<?php
return [
// Your Authgear project endpoint, e.g. https://my-project.authanyid.com
'project_url' => env('AUTHGEAR_PROJECT_URL', ''),
'client_id' => env('AUTHGEAR_APP_CLIENT_ID', ''),
'client_secret' => env('AUTHGEAR_APP_CLIENT_SECRET', ''),
'redirect_uri' => env('AUTHGEAR_APP_REDIRECT_URI', ''),
// OAuth 2.0 / OIDC scopes requested during authorization.
'scopes' => env('AUTHGEAR_SCOPES', 'openid email profile'),
// OIDC endpoints derived from the project URL.
'authorize_endpoint' => env('AUTHGEAR_PROJECT_URL', '').'/oauth2/authorize',
'token_endpoint' => env('AUTHGEAR_PROJECT_URL', '').'/oauth2/token',
'userinfo_endpoint' => env('AUTHGEAR_PROJECT_URL', '').'/oauth2/userInfo',
'end_session_endpoint' => env('AUTHGEAR_PROJECT_URL', '').'/oauth2/end_session',
];
将您的 Authany 应用凭据添加到项目的 .env 文件中:
AUTHGEAR_PROJECT_URL=
AUTHGEAR_APP_CLIENT_ID=
AUTHGEAR_APP_CLIENT_SECRET=
AUTHGEAR_APP_REDIRECT_URI=http://localhost:8000/oauth/callback
说明
您的 Authany 项目 URL,就是任意一个端点 URL 的主机名。例如,如果某个项目的授权端点是 https://laravel-app.authanyid.com/oauth2/authorize,那么该项目的 URL 就是 https://laravel-app.authanyid.com。
步骤 6:绑定 OAuth 提供方¶
我们将使用 league/oauth2-client 包与 Authany 的 OAuth 端点通信。安装该包:
在服务容器中绑定一个配置好的 GenericProvider 实例,以便将其注入控制器。这样可以把 OAuth 的配置集中在一处,也让控制器便于测试。
打开 app/Providers/AppServiceProvider.php,在 register() 方法中添加该绑定:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use League\OAuth2\Client\Provider\GenericProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(GenericProvider::class, function () {
return new GenericProvider([
'clientId' => config('authgear.client_id'),
'clientSecret' => config('authgear.client_secret'),
'redirectUri' => config('authgear.redirect_uri'),
'urlAuthorize' => config('authgear.authorize_endpoint'),
'urlAccessToken' => config('authgear.token_endpoint'),
'urlResourceOwnerDetails' => config('authgear.userinfo_endpoint'),
]);
});
}
public function boot(): void
{
//
}
}
步骤 7:发送 OAuth 授权请求¶
在这一步中,您将创建一个路由,把用户从您的应用重定向到 Authany 的授权页面,用户会在该页面上授权您的应用访问其账户。如果您以前用 Google 账号登录过某个网站,应该见过类似的授权页面。
创建一个控制器来处理所有 OAuth 操作:
打开 app/Http/Controllers/OAuthController.php,通过构造函数注入 provider,并添加 startAuthorization() 方法:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider;
class OAuthController extends Controller
{
public function __construct(private GenericProvider $provider)
{
}
public function startAuthorization(Request $request): RedirectResponse
{
$authorizationUrl = $this->provider->getAuthorizationUrl([
'scope' => config('authgear.scopes'),
]);
// Persist the state value to validate it on the callback (CSRF protection).
$request->session()->put('oauth2state', $this->provider->getState());
return redirect()->away($authorizationUrl);
}
}
startAuthorization() 会在重定向之前把 OAuth 的 state 值存入会话,您将在回调中校验它,以防范 CSRF 攻击。
现在注册路由。打开 routes/web.php,将其内容替换为:
<?php
use App\Http\Controllers\OAuthController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('index');
});
// Authgear OAuth 2.0 / OIDC flow.
Route::get('/login', [OAuthController::class, 'startAuthorization'])->name('login');
Route::get('/oauth/callback', [OAuthController::class, 'handleRedirect']);
Route::post('/logout', [OAuthController::class, 'logout'])->name('logout');
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
/login 路由被命名为 login,这样 Breeze 的 auth 中间件就会将未登录用户重定向到 Authany。您将在接下来的步骤中添加 handleRedirect() 和 logout() 方法。
由于现在 Authany 是唯一的身份提供方,不再需要 Breeze 的本地身份验证路由。请直接删除 routes/auth.php(该文件定义了注册、登录、密码重置和邮箱验证等路由)。上面的路由文件已经去掉了 Breeze 添加到 web.php 中的 require __DIR__.'/auth.php'; 这一行。如果想让项目更整洁,您也可以删除 app/Http/Controllers/Auth/ 目录下对应的控制器及其 Blade 视图。
此时访问 /login 应该会重定向到 Authany 的授权页面。

步骤 8:处理重定向¶
用户授权您的应用后,Authany 会带着授权码重定向回 /oauth/callback。handleRedirect() 方法会校验请求、用授权码换取访问令牌,并获取用户信息。
在 OAuthController 中添加 handleRedirect() 方法:
public function handleRedirect(Request $request): RedirectResponse
{
$state = $request->query('state');
$expectedState = $request->session()->pull('oauth2state');
if (empty($state) || ! is_string($expectedState) || ! hash_equals($expectedState, $state)) {
return redirect('/')->withErrors(['oauth' => 'Invalid OAuth state. Please try logging in again.']);
}
$code = $request->query('code');
if (empty($code)) {
return redirect('/')->withErrors(['oauth' => 'Authorization code missing.']);
}
try {
$accessToken = $this->provider->getAccessToken('authorization_code', [
'code' => $code,
]);
$userInfo = $this->provider->getResourceOwner($accessToken)->toArray();
} catch (IdentityProviderException $e) {
report($e);
return redirect('/')->withErrors(['oauth' => 'Failed to authenticate with Authgear.']);
}
if (empty($userInfo['sub'])) {
return redirect('/')->withErrors(['oauth' => 'Authgear did not return a user identifier.']);
}
$user = $this->findOrCreateUser($userInfo);
Auth::guard('web')->login($user);
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
这个方法做了几件事来保护您的应用:
- 校验
state。 使用hash_equals将返回的state与步骤 7 中存储的值进行比较。不匹配则拒绝该请求,从而阻止针对回调的 CSRF 攻击。 - 从请求对象读取,而不是
$_GET。 使用$request->query()可以让代码与 Laravel 的其他部分保持一致,并且便于测试。 - 优雅地处理失败。 在令牌交换或获取用户信息过程中出现的任何错误都会被记录,并转换为一个友好的重定向,而不是直接把异常信息输出到浏览器。
如果您输出 $userInfo 数组(dd($userInfo)),会看到 Authany 返回的声明:
[
"custom_attributes" => []
"email" => "users-email@gmail.com"
"email_verified" => true
"https://authgear.com/claims/user/can_reauthenticate" => true
"https://authgear.com/claims/user/is_anonymous" => false
"https://authgear.com/claims/user/is_verified" => true
"sub" => "e1234323-f123-4b99-91d8-c2ca55a6a3dc"
"updated_at" => 1683898685
]
sub 和 email_verified 这两个声明将驱动下一步的账号关联逻辑。
步骤 9:将 Authany 用户关联到 Laravel 会话¶
handleRedirect() 会调用 findOrCreateUser(),将 Authany 用户映射到本地的 Laravel 用户,这样您的应用就可以开启一个正常的已认证会话,并对受保护的路由进行守卫。
在 OAuthController 中添加 findOrCreateUser() 方法:
private function findOrCreateUser(array $userInfo): User
{
// Match on the stable Authgear subject identifier, never on email alone.
$user = User::query()->where('oauth_uid', $userInfo['sub'])->first();
if ($user) {
return $user;
}
// Link to an existing local account by email ONLY when Authgear reports
// the email as verified. Linking on an unverified email would allow
// account takeover.
if (! empty($userInfo['email']) && ($userInfo['email_verified'] ?? false) === true) {
$existing = User::query()->where('email', $userInfo['email'])->first();
if ($existing) {
$existing->oauth_uid = $userInfo['sub'];
$existing->save();
return $existing;
}
}
return User::create([
'name' => $userInfo['email'] ?? $userInfo['sub'],
'email' => $userInfo['email'] ?? null,
'oauth_uid' => $userInfo['sub'],
'password' => Hash::make(Str::random(40)),
]);
}
该方法解析用户的方式如下:
- 优先按
sub匹配。 Authany 的主体标识符(sub)对每个用户来说都是稳定且唯一的,因此是可靠的匹配键。而邮箱地址可能会变更或被重新分配。 - 仅在邮箱已验证时才按邮箱关联。 如果本地还没有用户拥有该
sub,则按邮箱关联到已有账号,但前提是email_verified为true。如果按未验证的邮箱进行关联,攻击者就有可能冒领他人的账号。 - 否则创建新用户。 新用户会获得一个随机的本地密码,但他们永远不会用到这个密码,因为他们始终通过 Authany 登录。
说明
本示例不会在会话中存储访问令牌或刷新令牌,也不会请求 offline_access 作用域。使用的作用域是 openid email profile。应用只在登录时读取一次用户身份,之后就依赖 Laravel 会话。
完整的 OAuthController 请参见这里。
现在运行应用,打开着陆页并点击登录链接。您会被重定向到 Authany 的授权页面。完成授权后,Authany 会将您带回回调路由,成功后您会进入 Breeze 仪表盘:

步骤 10:登出¶
要让用户登出,需要先清除本地的 Laravel 会话,然后结束 Authany 的会话,让用户彻底退出登录。请使用 OIDC 的 end_session 端点,而不是撤销令牌,因为应用本身并没有持有任何令牌可供撤销。
在 OAuthController 中添加 logout() 方法:
public function logout(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// If Authgear is configured, end its session too for a full sign-out.
if (! empty(config('authgear.project_url'))) {
return redirect()->away(config('authgear.end_session_endpoint'));
}
return redirect('/');
}
POST /logout 路由已经在步骤 7 中的 routes/web.php 里配置好了,因此任何向 logout 路由发送 POST 请求的 Breeze 登出按钮,都会触发这个方法。
验证功能是否正常¶
示例仓库在 tests/Feature/OAuthTest.php 中提供了一个功能测试,覆盖了 state 校验、授权码交换和账号关联。运行测试套件,确认流程符合预期:
接下来做什么¶
请尝试在 Authany 管理台中启用不同的登录方式(双重验证、无密码登录、手机 OTP 等),无需修改应用中的任何代码。
本指南中的两个安全加固要点,值得在任何生产环境集成中保留:在回调中校验 OAuth 的 state,以及仅在 Authany 报告邮箱已验证时才按邮箱关联账号。
完整的示例应用代码请参见 Laravel 示例 GitHub 仓库。