本文目录导读:

我来详细介绍在Symfony项目中集成SAML和单点登录(SSO)的实现方案。
SAML SSO基础概念
SAML SSO流程
sequenceDiagram
participant User as 用户
participant SP as Symfony (SP)
participant IDP as 身份提供商 (IdP)
User->>SP: 访问受保护资源
SP->>User: 重定向到IdP
User->>IDP: 认证请求
IDP->>User: 登录表单
User->>IDP: 提交凭证
IDP->>User: SAML断言
User->>SP: 提交SAML响应
SP->>User: 创建会话,访问资源
Symfony SAML集成方案
使用 SAML Bundle
安装依赖
composer require hslavich/oneloginsaml-bundle
配置 SAML Bundle
config/packages/hslavich_saml.yaml
hslavich_saml:
sp:
entityId: 'https://your-app.com/saml/metadata'
assertionConsumerService:
url: 'https://your-app.com/saml/acs'
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
singleLogoutService:
url: 'https://your-app.com/saml/logout'
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
idp:
entityId: 'https://idp.example.com/metadata'
singleSignOnService:
url: 'https://idp.example.com/sso'
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
singleLogoutService:
url: 'https://idp.example.com/slo'
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
security:
keys:
- private_key_path: '%kernel.project_dir%/config/saml/private.key'
certificate_path: '%kernel.project_dir%/config/saml/public.cert'
安全配置
config/packages/security.yaml
security:
firewalls:
main:
pattern: ^/
saml:
# 认证提供者
provider: user_provider
# SAML认证检查
check_path: saml_acs
# 登录路径
login_path: /saml/login
# 使用默认的认证成功处理器
default_target_path: /
logout:
path: /logout
target: /
providers:
user_provider:
entity:
class: App\Entity\User
property: samlIdpUsername
access_control:
- { path: ^/saml/login, roles: PUBLIC_ACCESS }
- { path: ^/saml/metadata, roles: PUBLIC_ACCESS }
- { path: ^/saml/acs, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_USER }
创建认证控制器
src/Controller/SecurityController.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// 重定向到SAML SSO
return $this->redirectToRoute('saml_login');
}
#[Route('/saml/login', name: 'saml_login')]
public function samlLogin(): Response
{
// SAML登录由Bundle自动处理
return $this->render('security/login.html.twig');
}
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
// Symfony会自动处理注销
throw new \LogicException('This method can be blank');
}
#[Route('/saml/acs', name: 'saml_acs')]
public function assertionConsumerService(): Response
{
// SAML断言处理由Bundle自动处理
throw new \LogicException('This method can be blank');
}
}
用户实体和提供者
src/Entity/User.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $samlIdpUsername = null;
#[ORM\Column(type: 'json')]
private array $roles = [];
#[ORM\Column(type: 'string', nullable: true)]
private ?string $password = null;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getSamlIdpUsername(): ?string
{
return $this->samlIdpUsername;
}
public function setSamlIdpUsername(?string $samlIdpUsername): self
{
$this->samlIdpUsername = $samlIdpUsername;
return $this;
}
public function getUserIdentifier(): string
{
return (string) $this->email;
}
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): self
{
$this->password = $password;
return $this;
}
public function eraseCredentials(): void
{
// 清除敏感数据
}
}
SAML认证器
src/Security/SamlAuthenticator.php
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class SamlAuthenticator extends AbstractLoginFormAuthenticator
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function authenticate(Request $request): Passport
{
// 从SAML断言中获取用户信息
$samlAttributes = $request->getSession()->get('saml_attributes');
if (!$samlAttributes) {
throw new CustomUserMessageAuthenticationException('No SAML attributes found');
}
$email = $samlAttributes['email'] ?? null;
if (!$email) {
throw new CustomUserMessageAuthenticationException('Email not provided in SAML response');
}
return new SelfValidatingPassport(
new UserBadge($email, function($userIdentifier) {
return $this->findOrCreateUser($userIdentifier);
})
);
}
private function findOrCreateUser(string $email): \App\Entity\User
{
$user = $this->entityManager->getRepository(\App\Entity\User::class)
->findOneBy(['email' => $email]);
if (!$user) {
$user = new \App\Entity\User();
$user->setEmail($email);
$user->setRoles(['ROLE_USER']);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return $user;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // 使用默认的success handler
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
throw $exception;
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate('app_login');
}
}
高级配置和优化
多IdP支持
# config/packages/hslavich_saml.yaml
hslavich_saml:
sp:
entityId: 'https://your-app.com/saml/metadata'
# ... SP配置
idps:
main_idp:
entityId: 'https://main-idp.com/metadata'
singleSignOnService:
url: 'https://main-idp.com/sso'
# ...
backup_idp:
entityId: 'https://backup-idp.com/metadata'
singleSignOnService:
url: 'https://backup-idp.com/sso'
# ...
SAML属性映射
// src/EventListener/SamlEventListener.php
namespace App\EventListener;
use Hslavich\OneloginSamlBundle\Event\SamlLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SamlEventListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
SamlLoginEvent::class => 'onSamlLogin',
];
}
public function onSamlLogin(SamlLoginEvent $event)
{
$user = $event->getUser();
$attributes = $event->getAttributes();
// 映射SAML属性到用户实体
if (isset($attributes['firstName'])) {
$user->setFirstName($attributes['firstName'][0]);
}
if (isset($attributes['lastName'])) {
$user->setLastName($attributes['lastName'][0]);
}
if (isset($attributes['groups'])) {
$this->mapGroupsToRoles($user, $attributes['groups']);
}
}
}
自定义认证成功处理器
// src/Security/SamlAuthenticationSuccessHandler.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
class SamlAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
{
$user = $token->getUser();
// 更新最后登录时间
$user->setLastLogin(new \DateTime());
// 记录审计日志
$this->logAudit('USER_LOGIN_SUCCESS', $user->getEmail());
// 重定向到原始请求的URL或默认页面
$targetPath = $request->getSession()->get('_security.main.target_path');
return $targetPath ? new RedirectResponse($targetPath) : new RedirectResponse('/');
}
}
生成SAML证书
# 生成私钥 openssl genrsa -out config/saml/private.key 2048 # 生成证书签名请求 openssl req -new -key config/saml/private.key -out config/saml/certificate.csr # 生成自签名证书 openssl x509 -req -days 365 -in config/saml/certificate.csr -signkey config/saml/private.key -out config/saml/public.cert # 设置权限 chmod 600 config/saml/private.key chmod 644 config/saml/public.cert
测试和调试
测试SAML配置
// src/Controller/SamlController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SamlController extends AbstractController
{
#[Route('/saml/metadata', name: 'saml_metadata')]
public function metadata(): Response
{
// 返回SAML元数据XML
$metadata = $this->get('hslavich_saml.auth')->getSettings()->getSPMetadata();
return new Response($metadata, 200, ['Content-Type' => 'text/xml']);
}
#[Route('/saml/debug', name: 'saml_debug')]
public function debug(): Response
{
$auth = $this->get('hslavich_saml.auth');
$settings = $auth->getSettings();
$debugInfo = [
'sp_entity_id' => $settings->getSPData()['entityId'],
'idp_entity_id' => $settings->getIdPData()['entityId'],
'certificate_valid' => $auth->checkSettings(),
'session_info' => $this->getUser() ? [
'username' => $this->getUser()->getUserIdentifier(),
'roles' => $this->getUser()->getRoles()
] : null
];
return $this->json($debugInfo);
}
}
使用测试IdP
# 使用 TestShib 或 SimpleSAMLphp 作为测试IdP
# config/packages/hslavich_saml.yaml (测试环境)
when@test:
hslavich_saml:
idp:
entityId: 'https://idp.testshib.org/idp/shibboleth'
singleSignOnService:
url: 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'
常见问题解决
时钟偏差
# config/packages/hslavich_saml.yaml
hslavich_saml:
security:
# 允许的时间偏差(秒)
wantAssertionsEncrypted: false
strict: true
# 增加时间偏差容错
allowedClockDrift: 60
Session问题
# config/packages/framework.yaml
framework:
session:
handler_id: ~
cookie_secure: auto
cookie_samesite: lax
gc_maxlifetime: 3600
调试日志
# config/packages/monolog.yaml
monolog:
channels: ['saml']
handlers:
saml:
type: stream
path: '%kernel.logs_dir%/saml_%kernel.environment%.log'
level: debug
channels: ['saml']
这个完整的解决方案提供了Symfony项目集成SAML SSO的所有必要组件,从基础配置到高级特性,包括安全考虑和调试工具。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播