PHP单点登录(SSO)实现全攻略:从原理到代码实战
目录导读
- 什么是单点登录(SSO)?为什么需要它?
- SSO核心机制:Cookie、Session与Token的三角关系
- PHP实现SSO的三种主流方案对比
- 方案A:基于共享Session(同域)
- 方案B:基于中央认证服务器(CAS)
- 方案C:基于JWT的无状态SSO
- 手写一个轻量级PHP SSO系统(代码+步骤)
- SSO安全陷阱与解决方案(CSRF、XSS、重放攻击)
- SSO常见问题问答(FAQ)
什么是单点登录(SSO)?为什么需要它?
想象一下:你公司有OA、CRM、财务系统三个独立Web应用,每个都要单独输账号密码,员工每天光登录就要花5分钟,IT部门还要管理三套密码策略——这就是典型的“多点登录”痛点。

单点登录(Single Sign-On) 的核心价值在于:一次认证,全网通行,用户在任意一个子系统登录后,访问其他子系统时无需再次输入凭证,系统自动识别身份。
PHP实现SSO的三大场景:
- 企业内网多应用整合(如:OA+ERP+邮件)
- 多域名商城/平台(如:主站+子站+移动端H5)
- 微服务架构下的统一用户认证(常见于Laravel + API)
关键认知:SSO不是一种固定技术,而是一种认证架构模式,PHP中的实现方式取决于你的系统分布情况(同域?跨域?)。
SSO核心机制:Cookie、Session与Token的三角关系
要理解SSO,必须先理清这三者:
| 机制 | 存储位置 | 生命周期 | SSO中的角色 |
|---|---|---|---|
| Cookie | 客户端浏览器 | 可持久化(如7天) | 携带会话标识(Session ID或Token) |
| Session | 服务器端(文件/Redis/数据库) | 默认过期(通常30分钟) | 存储用户状态信息 |
| Token | 客户端(或Redis) | 可自定义过期时间 | 无状态认证的核心(如JWT) |
SSO核心矛盾点:Cookie的域限制——浏览器对domain.a.com下发的Cookie,在domain.b.com下不会被发送,这就是为什么跨域SSO需要“中央认证服务器”或“Token + 跨域共享”方案。
PHP实现SSO的三种主流方案对比
方案A:基于共享Session(仅限同域)
// 前提:所有子系统在同一个域下(如 app.company.com/oa、app.company.com/crm)
// 通过共享Redis存储Session
session_save_path('tcp://redis-server:6379');
session_start();
$_SESSION['user_id'] = 123; // 所有子系统共用一个Session池
优点:实现最简单,无额外开发量。
缺点:无法跨域(子域不同Cookies仍隔离),若子域为 oa.company.com 与 crm.company.com,需设置 session_set_cookie_params(['domain' => '.company.com']) 才行。
方案B:基于中央认证服务器(CAS)—— 最经典
流程:
1. 用户访问子系统A → 未登录 → 302重定向到 认证中心
2. 认证中心显示登录页 → 用户输入凭证 → 验证成功
3. 认证中心生成 Service Ticket(ST) → 通过URL参数回跳子系统A
4. 子系统A后台请求认证中心验证ST → 验证通过 → 建立本地Session
5. 访问子系统B → 同样重定向到认证中心 → 认证中心发现已有登录态 → 直接颁发ST → 回跳B
优点:跨域完全支持,安全性高。
缺点:需要单独维护认证中心服务,代码量较大。
方案C:基于JWT的无状态SSO(现代API微服务首选)
// 用户登录后,认证中心签发一个JWT: $token = JWT::encode(['uid' => 123, 'exp' => time() + 3600], $secretKey); // 子系统间通过请求头或URL参数传递JWT,本地用公钥验签 $decoded = JWT::decode($token, $publicKey, ['HS256']);
优点:跨域/跨平台友好,无需共享Session,适合前后端分离。
缺点:Token无法主动吊销(除非做黑名单),需警惕Token泄露风险。
手写一个轻量级PHP SSO系统(代码+步骤)
我们以实现方案B(简化版CAS)为例,需两个角色:sso-server(认证中心)和 sso-client(两个子系统)。
步骤1:认证中心 sso-server.php
<?php
session_start();
// 模拟用户数据
$users = ['admin' => '123456'];
// 处理登录表单
if ($_POST['username'] ?? false) {
if ($users[$_POST['username']] ?? false == $_POST['password']) {
$_SESSION['user'] = $_POST['username'];
// 生成一次性服务票据(ST)
$st = md5(uniqid());
$_SESSION['tickets'][] = $st;
// 回跳子系统
header("Location: {$_GET['redirect']}?st=$st");
exit;
}
}
// 校验ST接口(供子系统调用)
if ($_GET['validate'] ?? false) {
$st = $_GET['st'];
if (in_array($st, $_SESSION['tickets'] ?? [])) {
echo json_encode(['success' => true, 'user' => $_SESSION['user']]);
unset($_SESSION['tickets'][array_search($st, $_SESSION['tickets'])]);
} else {
echo json_encode(['success' => false]);
}
exit;
}
if ($_SESSION['user'] ?? false) {
$st = md5(uniqid()); $_SESSION['tickets'][] = $st;
header("Location: {$_GET['redirect']}?st=$st");
exit;
}
// 显示登录表单
echo "<form method='post'><input name='username' /><input name='password' /><button>登录</button></form>";
步骤2:子系统客户端 sso-client.php
<?php
session_start();
// 收到ST票据则去认证中心校验
if ($_GET['st'] ?? false) {
$verify = file_get_contents("http://sso.example.com/sso-server.php?validate=1&st={$_GET['st']}");
$result = json_decode($verify, true);
if ($result['success']) $_SESSION['user'] = $result['user'];
}
// 未登录则跳转认证中心
if (!$_SESSION['user'] ?? false) {
header("Location: http://sso.example.com/sso-server.php?redirect=" . urlencode("http://sub.example.com/client.php"));
exit;
}
echo "欢迎 " . $_SESSION['user'] . " 登录子系统!";
步骤3:部署测试
- 将
sso-server.php放在 认证服务器(如sso.example.com) - 将两个
sso-client.php分别放在oa.example.com和crm.example.com - 访问任一子站 → 自动跳转中央登录 → 登录后回跳 → 直接访问另一子站已自动登录
SSO安全陷阱与解决方案
| 风险 | 说明 | 应对措施 |
|---|---|---|
| CSRF(跨站请求伪造) | 攻击者诱导用户点击链接触发登录回跳 | 在认证中心生成随机state参数,回跳时校验 |
| XSS(跨站脚本) | 攻击者窃取ST票据 | 所有输出的用户数据用htmlspecialchars()转义 |
| 重放攻击 | 同一ST被多次使用 | 设计ST为一次性,校验后立即销毁(代码已实现) |
| Token/ST泄露 | 通过HTTP明文传输 | 全站强制HTTPS,不允许HTTP回退 |
| 会话固定攻击 | 攻击者提前伪造Session ID | 登录成功后必须调用session_regenerate_id() |
SSO常见问题问答(FAQ)
Q1:PHP中SSO一定要用Redis吗?
不一定,若仅同域,使用文件Session即可;若跨域,共享Session才需要Redis,但生产环境建议使用Redis,因为支持过期时间、持久化与分布式。
Q2:考虑使用现成的PHP SSO库?
推荐:
- phpCAS(官方CAS协议库)
- firebase/php-jwt(JWT生成/验证)
- Laravel Passport(Laravel生态的OAuth2/SSO方案) 不建议重复造轮子,除非你只想学习原理。
Q3:多域名(如主站.com和子站.cn)能实现SSO吗?
可以,但Cookie无法跨顶级域名,必须使用方案B(CAS)或方案C(JWT),方案C通常配合前置Nginx将JWT透传,或前端将JWT存localStorage通过fetch头传递。
Q4:用户退出登录,如何做到所有子系统同时退出?
这是SSO最棘手的问题,常见做法:
- 认证中心设置
logout接口,回调所有已登录子系统(需维护子系统注册表)- 使用JWT时,维护一个Redis黑名单(但在无状态场景下会增加复杂度)
Q5:SSO性能瓶颈在哪里?如何优化?
每次访问子系统都要查询认证中心(或验签JWT),优化:子系统本地缓存用户信息(如Session),只在初次请求时验证ST;JWT则本地验签无需网络IO。
PHP单点登录的实现路径取决于你的系统架构——同域选共享Session,跨域且希望可控选CAS,现代API/微服务选JWT,理解核心原理(认证中心、票据、回调)后,你既能使用现成库,也能自己动手造一个满足特定需求的SSO,安全永远是第一位的,任何SSO实现都必须全站HTTPS并对ST/Token生命周期做严格限制。