本文目录导读:

在PHP项目中实现SAML(安全断言标记语言)认证,通常有两种主流方式:使用现成的库(如onelogin/php-saml)或使用轻量级库(如lightSAML),以下是完整的实现指南,以最流行的onelogin/php-saml为例。
核心概念
SAML工作流程中涉及三个角色:
- SP(服务提供者):你的PHP应用
- IdP(身份提供者):如Azure AD、Okta、Keycloak等
- 用户(浏览器):最终使用者
使用 onelogin/php-saml 实现
安装依赖
composer require onelogin/php-saml
项目结构建议
your-app/
├── saml/
│ ├── settings.php # SAML配置
│ ├── acs.php # Assertion Consumer Service(断言消费者服务)
│ ├── login.php # SP发起登录
│ ├── logout.php # 单点登出
│ └── metadata.php # 生成SP元数据
├── index.php # 受保护页面示例
└── vendor/
核心配置 (saml/settings.php)
<?php
// 定义常量
define('SAML_PATH', __DIR__);
define('SP_BASE_URL', 'https://your-app.com/saml');
// SP(服务提供者)配置
$sp_settings = [
'entityId' => SP_BASE_URL . '/metadata.php',
'assertionConsumerService' => [
'url' => SP_BASE_URL . '/acs.php',
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
],
'singleLogoutService' => [
'url' => SP_BASE_URL . '/logout.php',
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
],
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
'x509cert' => '', // SP的证书(如果需要对SAML响应签名)
'privateKey' => '', // SP的私钥
];
// IdP(身份提供者)配置 - 从IdP获取
$idp_settings = [
'entityId' => 'https://your-idp.com/auth/realms/master', // IdP的实体ID
'singleSignOnService' => [
'url' => 'https://your-idp.com/auth/realms/master/protocol/saml',
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
],
'singleLogoutService' => [
'url' => 'https://your-idp.com/auth/realms/master/protocol/saml/logout',
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
],
'x509cert' => file_get_contents(SAML_PATH . '/certs/idp.crt'), // IdP的公钥证书
];
// 合并配置
$settings = new \OneLogin\Saml2\Settings([
'sp' => $sp_settings,
'idp' => $idp_settings,
'security' => [
'authnRequestsSigned' => true, // 对SAML请求签名
'wantAssertionsSigned' => true, // 要求断言被签名
'wantNameIdEncrypted' => false, // 是否加密NameID
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
],
'strict' => true, // 生产环境建议true
'debug' => true, // 调试用,生产关闭
]);
return $settings;
SP发起登录 (saml/login.php)
<?php
session_start();
require_once 'vendor/autoload.php';
$settings = require 'saml/settings.php';
$auth = new \OneLogin\Saml2\Auth($settings);
// 可选:保存当前URL,登录后重定向
$_SESSION['return_to'] = $_GET['return_to'] ?? '/index.php';
// 检查是否有登录失败标记
if (isset($_GET['error'])) {
echo "认证失败: " . htmlspecialchars($_GET['error_msg']);
exit;
}
// 重定向到IdP的SSO URL
$auth->login(SP_BASE_URL . '/acs.php'); // 参数:重定向目标URL
处理SAML响应 (saml/acs.php)
<?php
session_start();
require_once 'vendor/autoload.php';
$settings = require 'saml/settings.php';
$auth = new \OneLogin\Saml2\Auth($settings);
// 处理SAML响应
$auth->processResponse();
// 检查错误
$errors = $auth->getErrors();
if (!empty($errors)) {
echo "SAML响应验证失败: " . implode(', ', $errors);
exit;
}
// 获取用户信息
$attributes = $auth->getAttributes();
$nameId = $auth->getNameId();
$sessionIndex = $auth->getSessionIndex();
// 在PHP Session中保存用户信息
$_SESSION['auth'] = true;
$_SESSION['user'] = [
'name_id' => $nameId,
'email' => $attributes['email'][0] ?? null,
'name' => $attributes['firstName'][0] . ' ' . $attributes['lastName'][0],
'groups' => $attributes['groups'] ?? [],
];
// 重定向到原来请求的页面
$redirect = $_SESSION['return_to'] ?? '/index.php';
unset($_SESSION['return_to']);
header('Location: ' . $redirect);
exit;
单点登出 (saml/logout.php)
<?php
session_start();
require_once 'vendor/autoload.php';
$settings = require 'saml/settings.php';
$auth = new \OneLogin\Saml2\Auth($settings);
// 检查是否有IdP发起的登出请求
if (isset($_GET['SAMLRequest'])) {
// IdP发起的登出
$auth->processSLO();
session_destroy();
header('Location: /index.php');
exit;
}
// SP发起的登出
$nameId = $_SESSION['user']['name_id'] ?? null;
$sessionIndex = $_SESSION['user']['session_index'] ?? null;
// 清除本地Session
session_destroy();
// 重定向到IdP的登出URL
$auth->logout('/index.php', $nameId, $sessionIndex);
exit;
生成SP元数据 (saml/metadata.php)
<?php
require_once 'vendor/autoload.php';
$settings = require 'saml/settings.php';
$auth = new \OneLogin\Saml2\Auth($settings);
$metadata = $auth->getSettings()->getSPMetadata();
// 验证元数据
$errors = $auth->getSettings()->validateMetadata($metadata);
if (empty($errors)) {
header('Content-Type: text/xml');
echo $metadata;
} else {
echo "元数据验证错误: " . implode(', ', $errors);
}
受保护页面示例 (index.php)
<?php
session_start();
if (!isset($_SESSION['auth']) || !$_SESSION['auth']) {
// 未登录,重定向到登录
header('Location: /saml/login.php?return_to=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
// 用户已登录
$user = $_SESSION['user'];
?>
<!DOCTYPE html>
<html>
<head>受保护的页面</title>
</head>
<body>
<h1>欢迎, <?= htmlspecialchars($user['name']) ?></h1>
<p>姓名: <?= htmlspecialchars($user['name']) ?></p>
<p>邮箱: <?= htmlspecialchars($user['email']) ?></p>
<p>用户组: <?= implode(', ', $user['groups']) ?></p>
<p><a href="/saml/logout.php">登出</a></p>
</body>
</html>
与IdP集成步骤
- 获取SP元数据:访问
https://your-app.com/saml/metadata.php,将XML内容导入IdP。 - 配置IdP:在IdP侧(如Azure AD):
- 上传SP元数据或手动配置ACS URL、Audience URI
- 注册用户属性映射(如 email->email, firstName->given_name)
- 获取IdP配置:从IdP获取:
- IdP元数据URL或XML内容
- 实体ID(Issuer)
- SSO服务URL
- 签名证书(x509cert)
生产环境注意事项
- HTTPS强制使用:所有SAML端点和回调URL必须使用HTTPS。
- 保持时间同步:SAML使用时间戳,服务器时间偏差应<5分钟(使用NTP)。
- 严格模式:生产环境设置
strict => true。 - 防止CSRF:SAML Auth本身有RelayState机制防CSRF,但应用层仍需注意。
- Session管理:建议使用Redis等外部Session存储,便于跨实例共享。
- 错误处理:不要在生产环境显示详细错误信息。
常见问题调试
- 签名验证失败:检查证书格式(必须为PEM格式,以
-----BEGIN CERTIFICATE-----开头) - 时间偏差错误:运行
date命令检查服务器时间,配置NTP - 响应格式错误:在acs.php中添加
file_put_contents('/tmp/saml_response.xml', $_POST['SAMLResponse'])查看原始响应 - 属性映射问题:检查IdP是否发送了正确的属性,使用
$auth->getAttributes()打印
替代方案
- lightSAML:更轻量,依赖少,适合简单场景
composer require lightsaml/lightsaml
- SimpleSAMLphp:完整的SAML框架,功能强大但较重
- 自建实现:使用XML解析和OpenSSL函数手动处理(不推荐)
推荐使用onelogin/php-saml,因为它被广泛使用、维护良好、文档完善,且符合SAML 2.0标准。