PHP项目如何实现SAML?

wen java案例 2

本文目录导读:

PHP项目如何实现SAML?

  1. 核心概念
  2. 使用 onelogin/php-saml 实现
  3. 与IdP集成步骤
  4. 生产环境注意事项
  5. 常见问题调试
  6. 替代方案

在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集成步骤

  1. 获取SP元数据:访问 https://your-app.com/saml/metadata.php,将XML内容导入IdP。
  2. 配置IdP:在IdP侧(如Azure AD):
    • 上传SP元数据或手动配置ACS URL、Audience URI
    • 注册用户属性映射(如 email->email, firstName->given_name)
  3. 获取IdP配置:从IdP获取:
    • IdP元数据URL或XML内容
    • 实体ID(Issuer)
    • SSO服务URL
    • 签名证书(x509cert)

生产环境注意事项

  1. HTTPS强制使用:所有SAML端点和回调URL必须使用HTTPS。
  2. 保持时间同步:SAML使用时间戳,服务器时间偏差应<5分钟(使用NTP)。
  3. 严格模式:生产环境设置 strict => true
  4. 防止CSRF:SAML Auth本身有RelayState机制防CSRF,但应用层仍需注意。
  5. Session管理:建议使用Redis等外部Session存储,便于跨实例共享。
  6. 错误处理:不要在生产环境显示详细错误信息。

常见问题调试

  • 签名验证失败:检查证书格式(必须为PEM格式,以-----BEGIN CERTIFICATE-----开头)
  • 时间偏差错误:运行date命令检查服务器时间,配置NTP
  • 响应格式错误:在acs.php中添加file_put_contents('/tmp/saml_response.xml', $_POST['SAMLResponse'])查看原始响应
  • 属性映射问题:检查IdP是否发送了正确的属性,使用$auth->getAttributes()打印

替代方案

  1. lightSAML:更轻量,依赖少,适合简单场景
    composer require lightsaml/lightsaml
  2. SimpleSAMLphp:完整的SAML框架,功能强大但较重
  3. 自建实现:使用XML解析和OpenSSL函数手动处理(不推荐)

推荐使用onelogin/php-saml,因为它被广泛使用、维护良好、文档完善,且符合SAML 2.0标准。

抱歉,评论功能暂时关闭!