单点登录(SSO)的核心目标是让用户在一个系统登录后,访问其他关联系统无需重复输入账号密码即可自动完成身份校验。在PHP开发中,跨域名场景下由于浏览器同源策略限制,无法直接共享Cookie,因此需要借助ticket验证机制实现会话同步。

单点登录核心原理
跨域名单点登录的核心逻辑分为三个角色:认证中心(SSO Server)、业务系统A(Client A)、业务系统B(Client B)。整体流程如下:
- 用户访问Client A,未登录则跳转至SSO Server登录页
- 用户在SSO Server完成登录,服务端生成全局会话和唯一ticket
- SSO Server将ticket作为参数回跳至Client A的回调地址
- Client A携带ticket向SSO Server发起校验请求,验证通过后创建本地会话
- 用户访问Client B时,重复上述ticket校验流程,无需再次登录
核心数据表设计
首先需要设计存储用户会话和ticket的数据表,这里给出核心表结构:
| 表名 | 字段 | 说明 |
|---|---|---|
| sso_ticket | ticket | 唯一票据字符串,用于校验身份 |
| user_id | 关联的用户ID | |
| expire_time | ticket过期时间戳 | |
| is_used | ticket是否已使用,0未使用1已使用 | |
| sso_session | session_id | 全局会话ID |
| user_id | 关联的用户ID | |
| expire_time | 全局会话过期时间戳 |
关键功能代码实现
1. ticket生成逻辑
用户登录成功后,生成唯一且有过期时间的ticket,存储到数据库并返回给业务系统:
<?php
/**
* 生成单点登录ticket
* @param int $userId 用户ID
* @param int $expire 过期时长(秒),默认300秒
* @return string
*/
function generateSsoTicket($userId, $expire = 300) {
// 生成随机ticket字符串
$ticket = md5(uniqid($userId . time(), true));
$expireTime = time() + $expire;
// 存储ticket到数据库
$pdo = new PDO('mysql:host=127.0.0.1;dbname=sso_db;charset=utf8', 'root', '123456');
$sql = 'INSERT INTO sso_ticket (ticket, user_id, expire_time, is_used) VALUES (?, ?, ?, 0)';
$stmt = $pdo->prepare($sql);
$stmt->execute([$ticket, $userId, $expireTime]);
return $ticket;
}
?>
2. 业务系统校验ticket逻辑
业务系统接收到SSO Server返回的ticket后,向SSO Server发起校验请求,验证通过后创建本地会话:
<?php
/**
* 校验ticket并创建本地会话
* @param string $ticket 待校验的ticket
* @param string $ssoVerifyUrl SSO Server校验接口地址
* @return bool
*/
function verifyTicketAndLogin($ticket, $ssoVerifyUrl) {
if (empty($ticket)) {
return false;
}
// 向SSO Server发起校验请求
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ssoVerifyUrl . '?ticket=' . $ticket);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result['code'] != 200) {
return false;
}
// 校验通过,创建本地会话
session_start();
$_SESSION['user_id'] = $result['data']['user_id'];
$_SESSION['login_time'] = time();
return true;
}
// 使用示例
$ticket = $_GET['ticket'] ?? '';
$verifyUrl = 'http://sso.ipipp.com/verify_ticket';
if (verifyTicketAndLogin($ticket, $verifyUrl)) {
echo '登录成功,欢迎访问业务系统A';
} else {
echo '登录失败,请重新登录';
}
?>
3. SSO Server校验ticket接口
SSO Server接收业务系统的校验请求,验证ticket有效性并返回用户ID:
<?php
// SSO Server ticket校验接口
header('Content-Type: application/json; charset=utf-8');
$ticket = $_GET['ticket'] ?? '';
if (empty($ticket)) {
echo json_encode(['code' => 400, 'msg' => 'ticket不能为空']);
exit;
}
$pdo = new PDO('mysql:host=127.0.0.1;dbname=sso_db;charset=utf8', 'root', '123456');
// 查询ticket信息
$sql = 'SELECT * FROM sso_ticket WHERE ticket = ? AND is_used = 0 AND expire_time > ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$ticket, time()]);
$ticketInfo = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($ticketInfo)) {
echo json_encode(['code' => 401, 'msg' => 'ticket无效或已过期']);
exit;
}
// 标记ticket已使用,避免重复使用
$updateSql = 'UPDATE sso_ticket SET is_used = 1 WHERE id = ?';
$pdo->prepare($updateSql)->execute([$ticketInfo['id']]);
// 返回用户信息
echo json_encode([
'code' => 200,
'msg' => '校验成功',
'data' => ['user_id' => $ticketInfo['user_id']]
]);
?>
注意事项
- ticket必须设置较短的过期时间,且只能使用一次,避免被截获后重复使用
- SSO Server和业务系统之间的接口通信建议添加签名校验,防止请求被伪造
- 全局会话和本地会话的过期时间需要做好同步,避免出现状态不一致问题
- 跨域名场景下,SSO Server的域名需要和业务系统的域名做好DNS解析配置,确保网络互通
该方案适用于中小型系统的跨域名单点登录需求,若系统规模较大,可结合Redis存储会话信息,提升读写性能。