别再各写各的登录了!三步吃透PHP(Yii2)接入企业级SSO单点登录

admin 2026-07-01 05:50:17 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细讲解PHP(Yii2)框架接入企业级SSO单点登录的完整方案。核心内容包括SSO的Ticket认证机制、三层架构实现(接口定义-服务实现-控制器调用),以及企业级安全防护要点。文章提供了完整的代码示例,涵盖认证流程、用户信息同步和会话管理,并强调Ticket防重放、用户状态同步、全链路加密等安全准则。 综合评分: 82 文章分类: 应用安全,安全开发,解决方案,WEB安全,安全建设


cover_image

别再各写各的登录了!三步吃透 PHP (Yii2) 接入企业级 SSO 单点登录

原创

学习与分享 学习与分享

php的自我修养

2026年6月29日 08:30 广东

在小说阅读器读本章

去阅读

商务合作加微信:2230304070

学习与交流:PHP技术自我修养微信群

免费领取phpstorm正版激活码

获取地址:https://web.52shizhan.cn

以下文章正文

research and development

引言:为什么企业级应用需要 SSO?

在企业级应用群落中,孤立的账号体系是研发和用户的噩梦。当公司内部孵化出 OA、CRM、ERP 等多套系统时,如果用户在每个系统都需要重复输入账号密码,不仅体验极差,数据孤立、权限混乱、安全防线难以收拢等痛点也会接踵而至。

SSO(Single Sign-On,单点登录) 正是解决这一痛点的终极方案。它允许用户在多个互信的独立业务系统中,“一次登录,到处通行”。本文将带你从底层机制出发,用 PHP (Yii2 架构) 闭环实现一套工业级的 SSO 接入方案。

一、 核心复盘:SSO 认证究竟是如何运转的?

要接入 SSO,首先必须搞懂它的核心账密置换凭证 —— Ticket(授权码/票据)机制

我们以经典的中心化认证流为例,当用户访问受保护的业务系统时,其核心步骤如下:

  1. 路由拦截

    :用户访问业务系统 B。业务系统发现用户未登录,通过 302 重定向将用户引导至 SSO 认证中心

  2. 凭证分发

    :用户在 SSO 中心完成账号密码认证。SSO 中心验证通过后,生成一个短期内有效的随机凭证 Ticket,并带着这个 Ticket 重定向回业务系统 B 预留的回调地址。

  3. 后台置换

    :业务系统 B 拿到 Ticket 后,在后端(Server to Server)发起请求,向 SSO 中心验证 Ticket 的合法性,并换取当前用户的核心信息(如工号、邮箱、角色)。

  4. 本地授信

    :业务系统 B 验证无误,将用户信息写入本地 Session/Token,自动完成本地登录,页面正常响应。

二、 实战演练:三步打通 PHP 的 SSO 接入

为了让代码更具规范性和扩展性,我们采用 接口定义  服务实现  控制器调用 的三层架构。

步骤 1:定义统一的 SSO 契约接口

首先,我们定义一个规范的接口。不论未来的 SSO 协议是基于标准的 OIDC、OAuth2 还是自研的 CAS,业务层只需要关心这两个核心动作。

<?php
namespace&nbsp;app\components\sso;

interface&nbsp;SSOLoginInterface
{
&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 获取单点登录中心授权跳转地址
&nbsp; &nbsp; &nbsp;*&nbsp;@return&nbsp;string 跳转到SSO中心的完整URL
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;public&nbsp;function&nbsp;getAuthUrl():&nbsp;string;

&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 通过回调获得的 Ticket 换取 SSO 中心的用户信息
&nbsp; &nbsp; &nbsp;*&nbsp;@param&nbsp;string $ticket 认证票据
&nbsp; &nbsp; &nbsp;*&nbsp;@return&nbsp;array 用户基础信息数组
&nbsp; &nbsp; &nbsp;*&nbsp;@throws&nbsp;\Exception 校验失败时抛出异常
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;public&nbsp;function&nbsp;getInfoFromTicket(string $ticket):&nbsp;array;
}

步骤 2:落地具体对接的 SSO 服务类

这里我们模拟一个具体的 CompanySSOService。在实际生产环境下,你可以将这里的 curl 请求替换为你们公司的 SSO 中心真实 API。

<?php
namespaceapp\components\sso;

useYii;
useyii\base\Component;

class&nbsp;CompanySSOService&nbsp;extends&nbsp;Component&nbsp;implements&nbsp;SSOLoginInterface
{
&nbsp; &nbsp;&nbsp;public&nbsp;$ssoHost =&nbsp;'https://sso.company.com';
&nbsp; &nbsp;&nbsp;public&nbsp;$clientId =&nbsp;'crm_system_01';
&nbsp; &nbsp;&nbsp;public&nbsp;$clientSecret =&nbsp;'sso_secret_key_xxxxxx';

&nbsp; &nbsp;&nbsp;publicfunction&nbsp;getAuthUrl():&nbsp;string
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 告诉SSO中心,认证成功后跳回本地的什么地方
&nbsp; &nbsp; &nbsp; &nbsp; $callbackUrl = urlencode(Yii::$app->request->getHostInfo() .&nbsp;'/sso/callback');
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return"{$this->ssoHost}/login?client_id={$this->clientId}&redirect_uri={$callbackUrl}";
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;publicfunction&nbsp;getInfoFromTicket(string $ticket):&nbsp;array
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; $apiExhangeUrl =&nbsp;"{$this->ssoHost}/api/verify-ticket";

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 发起后端安全的 Server to Server 请求,杜绝前端伪造
&nbsp; &nbsp; &nbsp; &nbsp; $ch = curl_init();
&nbsp; &nbsp; &nbsp; &nbsp; curl_setopt($ch, CURLOPT_URL, $apiExhangeUrl);
&nbsp; &nbsp; &nbsp; &nbsp; curl_setopt($ch, CURLOPT_POST,&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; curl_setopt($ch, CURLOPT_RETURNTRANSFER,&nbsp;true);
&nbsp; &nbsp; &nbsp; &nbsp; curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'ticket'&nbsp;=> $ticket,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'client_id'&nbsp;=>&nbsp;$this->clientId,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'client_secret'&nbsp;=>&nbsp;$this->clientSecret,
&nbsp; &nbsp; &nbsp; &nbsp; ]));

&nbsp; &nbsp; &nbsp; &nbsp; $response = curl_exec($ch);
&nbsp; &nbsp; &nbsp; &nbsp; curl_close($ch);

&nbsp; &nbsp; &nbsp; &nbsp; $result = json_decode($response,&nbsp;true);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(empty($result) || !isset($result['success']) || !$result['success']) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownew&nbsp;\Exception("SSO凭证校验失败: "&nbsp;. ($result['message'] ??&nbsp;'未知错误'));
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;$result['data'];&nbsp;// 返回格式统一包含: username, email, truename
&nbsp; &nbsp; }
}

步骤 3:编写控制器,闭环回调登录逻辑

在 Yii2 框架中创建 SsoController。它是整个登录闭环的控制中枢,负责处理Ticket 的接收、新用户的自动常驻同步、以及本地会话的建立

<?php
namespaceapp\controllers;

useYii;
useyii\web\Controller;
useyii\web\BadRequestHttpException;
useapp\models\User;
useapp\components\sso\CompanySSOService;

class&nbsp;SsoController&nbsp;extends&nbsp;Controller
{
&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;*&nbsp;@return&nbsp;CompanySSOService
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;privatefunction&nbsp;getSsoService()
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 建议在 Yii 的 components 中配置注入,此处快速实例化演示
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnew&nbsp;CompanySSOService();
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 登录入口触发表
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;publicfunction&nbsp;actionLogin()
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 如果本地已经有登录态,直接去后台首页
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!Yii::$app->user->isGuest) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->redirect(['/dashboard']);
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 渲染登录引导页,将前端登录按钮的链接指向 SSO 授权页
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->render('login', [
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'ssoUrl'&nbsp;=>&nbsp;$this->getSsoService()->getAuthUrl()
&nbsp; &nbsp; &nbsp; &nbsp; ]);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* SSO 中心认证通过后的核心回调接口
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;publicfunction&nbsp;actionCallback()
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; $ticket = Yii::$app->request->get('ticket');

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(empty($ticket)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownew&nbsp;BadRequestHttpException('授权失败:缺失关键 ticket 参数');
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 1. 去 SSO 中心洗白,拿回用户的真实资料
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $ssoUser =&nbsp;$this->getSsoService()->getInfoFromTicket($ticket);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(empty($ssoUser['username'])) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->renderError('SSO 授信中心返回的用户数据不完整');
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 2. 统一格式规整(防止大小写引发的账号双胞胎问题)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $username = strtolower(trim($ssoUser['username']));

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 3. 检索本地账号仓库,不存在则自动激活“影子上游账号”
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user = User::find()->where(['username'&nbsp;=> $username])->one();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!$user) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user =&nbsp;new&nbsp;User();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user->username = $username;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user->email = $ssoUser['email'] ??&nbsp;"{$username}@company.com";
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user->realname = $ssoUser['truename'] ??&nbsp;'新员工';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user->role = User::ROLE_MEMBER;&nbsp;// 默认赋予普通成员权限
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $user->status = User::STATUS_ACTIVE;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(!$user->save()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownew&nbsp;\Exception("本地同步创建用户失败:"&nbsp;. json_encode($user->getErrors()));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 4. 正式签发本地登录会话(Session/Cookie 保持 30 天)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Yii::$app->user->login($user,&nbsp;3600&nbsp;*&nbsp;24&nbsp;*&nbsp;30);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 5. 顺滑流转至系统内部首页
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->redirect(['/dashboard']);

&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(\Exception&nbsp;$e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Yii::error("SSO登录异常挂起: "&nbsp;. $e->getMessage(),&nbsp;__METHOD__);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->renderError($e->getMessage());
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;privatefunction&nbsp;renderError($msg)
&nbsp; &nbsp;&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->render('error', [
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'message'&nbsp;=> $msg,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'retryUrl'&nbsp;=>&nbsp;$this->getSsoService()->getAuthUrl()
&nbsp; &nbsp; &nbsp; &nbsp; ]);
&nbsp; &nbsp; }
}

三、 架构师避坑:企业级 SSO 的三大防御工事

单点登录一旦被攻破,意味着黑客拿到了进入公司所有系统的万能钥匙。在生产环境中部署时,必须牢记以下三大安全增强准则:

1. Ticket 防重放与防篡改

  • 一次性失效

    :Ticket 在设计时,必须在 SSO 服务端执行 “阅后即焚” 策略。即任何一个 Ticket 只要被业务系统验证过一次,无论成功与否,在 Redis 中都必须立刻被判定为作废。

  • 时效极短化

    :Ticket 只是用于置换 Token 的临时媒介,有效期通常应该限制在 30 秒 ~ 1 分钟 之内。

2. 用户动态映射与状态同步

  • 唯一标识锚定

    :在多系统间匹配用户时,不要直接依赖极易被修改的手机号或用户名。推荐使用全局唯一的员工工号(Staff ID)或不可变的专属 UID 作为主键映射绑定。

  • 状态熔断

    :如果员工在 SSO 中心被 HR 标记为离职或禁用,当其再次访问业务系统时,业务系统如果直接读取本地 Session,会导致离职员工仍有系统权限。因此,本地 Session 存活期不宜过长,或者业务系统在核心节点需定期(如每 5 分钟)异步校验一次 SSO 态的有效性。

3. 全链路安全防御

  • 加签防伪造

    :SSO 中心和各业务系统之间,推荐使用基于 RSA 签名 或 JWT 非对称加密 的算法。即使 Ticket 被拦截,黑客在没有私钥的情况下也无法伪造用户信息。

  • 全站 HTTPS

    :由于全链路都在重定向传输敏感凭证,严禁在 HTTP 纯明文环境下裸奔,所有域名必须无条件强制开启 HTTPS。

四、 总结

通过上述的逻辑重构与代码补充,我们可以发现,PHP 在接入 SSO 时拥有天然的高效和灵活性。只要我们卡死回调接口,做好 Ticket 的后台二次验证和账号的动态同步,就可以快速让任何一个新旧 PHP 项目无缝融入到公司的全局统一账号生态中。

原文链接:

以上就是本篇文章的全部内容,希望各位程序员们努力提升个人技术。最后,小编温馨提示:每天阅读5分钟,每天学习一点点,每天进步一点点。

点个赞

再走吧


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:php的自我修养 学习与分享 学习与分享《别再各写各的登录了!三步吃透 PHP (Yii2) 接入企业级 SSO 单点登录》

评论:0   参与:  0