本文目录导读:

在 PHP 中实现身份联盟(Federation),通常指的是让用户使用其第三方账号(如 Google、微信、GitHub 等)或其他组织的身份系统(如企业 AD 账号)登录你的应用,同时你又能同步或创建对应的本地用户记录。
身份联盟的核心协议是 OAuth 2.0 和 OpenID Connect (OIDC),以及企业场景中的 SAML。
以下是 PHP 实现身份联盟的几种主流方案和核心步骤:
使用第三方库(最推荐、最省事)
对于大多数 PHP 项目(Laravel、Symfony 或原生),强烈建议使用封装完善的开源库,避免自己处理复杂的签名、加密和握手逻辑。
Laravel 框架:Socialite(OAuth) 或 Socialite Providers
-
适合场景:对接 Google、GitHub、微信、QQ、Facebook 等社交账号。
-
实现步骤:
-
安装:
composer require laravel/socialite -
在
config/services.php配置第三方应用的client_id和client_secret。 -
创建路由,重定向到第三方:
Route::get('/auth/redirect', function () { return Socialite::driver('google')->redirect(); }); -
创建回调路由,获取用户信息并创建本地用户:
Route::get('/auth/callback', function () { $user = Socialite::driver('google')->user(); // 根据 $user->email 或 $user->getId() 查找或创建本地 User 模型 $localUser = User::firstOrCreate( ['email' => $user->getEmail()], ['name' => $user->getName()] ); Auth::login($localUser); });
-
非框架(原生 PHP)或企业级:league/oauth2-client 或 phpseclib
- 适合场景:对接微软 Azure AD、Okta 或自建 OIDC 服务器。
- 优点:轻量级,不依赖框架。
- 实现:使用
league/oauth2-client中的GenericProvider进行授权码流程。
基于 OIDC(OpenID Connect)手动实现(理解原理)
如果不使用框架,或者需要对接企业内部的 IdP(身份提供商),你需要手动处理以下流程:
请求授权(重定向到 IdP)
用户点击“使用企业账号登录”,你重定向到 IdP 的授权端点:
$params = [
'client_id' => 'your_client_id',
'redirect_uri' => 'https://your-app.com/callback',
'response_type' => 'code',
'scope' => 'openid profile email',
'state' => bin2hex(random_bytes(16)), // 防止 CSRF
];
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' . http_build_query($params);
header('Location: ' . $url);
处理回调(交换 Token)
用户同意后,IdP 带着 code 和 state 重定向到你的回调地址,你必须验证 state,然后向 Token 端点 POST 请求换取 access_token 和 id_token。
验证 ID Token(关键步骤)
ID Token 是一个 JWT,你必须严格验证它的签名和 nonce,而不能直接相信它。
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// 1. 从 IdP 的 discovery 端点获取公钥 (JWKS)
$jwks = file_get_contents('https://your-idp.com/.well-known/jwks.json');
$keys = json_decode($jwks, true);
// 2. 解析 id_token
$jwt = $idToken;
$payload = JWT::decode($jwt, new Key($publicKey, 'RS256'));
// 3. 必须验证的 Claims
if ($payload->aud !== 'your_client_id') {
die('Invalid audience');
}
if ($payload->iss !== 'https://your-idp.com') {
die('Invalid issuer');
}
if ($payload->exp < time()) {
die('Token expired');
}
// 4. 使用 $payload->sub (用户唯一ID) 来绑定本地用户
本地用户同步(Provisioning)
身份联盟并不意味着你不需要本地数据库,通常你需要一个 users 表,并在其中存储 idp_provider 和 idp_subject 字段,实现“外键”映射。
-- 核心表结构
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE,
name VARCHAR(255),
-- 本地自有字段
);
CREATE TABLE user_identities (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
provider VARCHAR(50), -- 'google', 'azure', 'github'
provider_uid VARCHAR(255), -- IdP 返回的 sub 或 user id
created_at TIMESTAMP
);
登录逻辑:先查 user_identities 表,provider_uid 存在则登录;不存在则创建新用户并插入关联。
企业级(SAML 2.0)
如果对接的是老牌企业系统(如使用 ADFS 或 Shibboleth),则使用 SAML 协议。
- PHP 库:使用
onelogin/php-saml。 - 区别:SAML 使用 XML 和 SAMLResponse,流程通常是 SP(服务提供商)发起。
- 实现:配置
settings.json(包含 IdP 的实体 ID、SSO URL、X.509 证书),然后调用$auth->login()和$auth->processResponse()。
关键安全要点(必须做)
在 PHP 中实现身份联盟时,最容易出安全问题的地方在于信任外部输入:
- 只信任 HTTPS:确保所有授权码和 token 传输都走 HTTPS。
- 验证
state参数:防止 CSRF 跨站请求伪造攻击。 - 验证 JWT 签名:必须从 IdP 的 JWKS 端点动态获取公钥,不能硬编码或跳过验证。
- 注意
email_verified:如果使用邮箱作为联合主键,第三方邮箱未验证可能会被用于账号接管(Account Takeover),建议无论何时都以sub(用户唯一标识)作为关联本地用户的唯一键,而不是邮箱。 - 处理 Token 刷新:
access_token会过期,需要提供refresh_token来延长会话。
总结建议
- 有 Laravel? 直接用
Socialite,只写几行就能对接。 - 原生 PHP? 用
league/oauth2-client或firebase/php-jwt+paragonie/sodium处理。 - 企业内部系统? 用
onelogin/php-saml。 - 永远不要自己发明 OAuth/OIDC 协议,安全极难做对。
如果你能提供更具体的场景(比如要对接微信还是 Azure AD),我可以给你更精确的代码示例。