SSO 单点登录

SSO 的原理和实现方式

问题

说一说 SSO 单点登录的原理和实现方式。

解答

SSO(Single Sign-On)单点登录是指用户只需登录一次,就可以访问多个相互信任的应用系统。

为什么需要 SSO

假设公司有多个系统:OA、CRM、邮箱等。没有 SSO 时,用户访问每个系统都要登录一次。有了 SSO,登录一次就能访问所有系统。

同域 SSO

当所有系统在同一主域名下(如 a.example.comb.example.com),可以共享 Cookie。

// 登录成功后,设置 Cookie 到主域
document.cookie = 'token=xxx; domain=.example.com; path=/';

// 所有子域都能读取这个 Cookie

跨域 SSO(CAS 流程)

当系统在不同域名下,需要独立的认证中心(SSO Server)。

用户 -> 系统A -> SSO Server -> 系统A
         |           |
         |     登录验证
         |           |
         +<-- ticket --+

流程说明:

  1. 用户访问系统 A
  2. 系统 A 发现未登录,重定向到 SSO Server
  3. 用户在 SSO Server 登录
  4. SSO Server 生成 ticket,重定向回系统 A
  5. 系统 A 用 ticket 换取用户信息
  6. 用户访问系统 B 时,SSO Server 已有登录态,直接返回 ticket

前端实现示例

// 检查登录状态
function checkLogin() {
  const token = localStorage.getItem('token');
  if (!token) {
    // 未登录,跳转 SSO
    redirectToSSO();
  }
}

// 跳转到 SSO 登录页
function redirectToSSO() {
  const currentUrl = encodeURIComponent(window.location.href);
  const ssoUrl = `https://sso.example.com/login?redirect=${currentUrl}`;
  window.location.href = ssoUrl;
}

// SSO 回调处理(登录成功后跳回来)
function handleSSOCallback() {
  const params = new URLSearchParams(window.location.search);
  const ticket = params.get('ticket');
  
  if (ticket) {
    // 用 ticket 换取 token
    fetch('/api/auth/validate', {
      method: 'POST',
      body: JSON.stringify({ ticket })
    })
    .then(res => res.json())
    .then(data => {
      localStorage.setItem('token', data.token);
      // 清除 URL 中的 ticket
      window.history.replaceState({}, '', window.location.pathname);
    });
  }
}

SSO Server 端简化示例

// Express 示例
const sessions = new Map(); // 存储登录态

// 登录接口
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  if (validateUser(username, password)) {
    const sessionId = generateId();
    sessions.set(sessionId, { username, loginTime: Date.now() });
    
    // 设置 SSO 域的 Cookie
    res.cookie('sso_session', sessionId, { httpOnly: true });
    
    // 生成 ticket 并重定向
    const ticket = generateTicket(sessionId);
    res.redirect(`${req.query.redirect}?ticket=${ticket}`);
  }
});

// 验证 ticket 接口
app.post('/validate', (req, res) => {
  const { ticket } = req.body;
  const sessionId = validateTicket(ticket);
  
  if (sessionId && sessions.has(sessionId)) {
    const user = sessions.get(sessionId);
    const token = generateJWT(user);
    res.json({ token });
  } else {
    res.status(401).json({ error: 'Invalid ticket' });
  }
});

单点登出

用户在一个系统登出时,需要通知所有系统清除登录态。

// 登出时通知 SSO Server
function logout() {
  localStorage.removeItem('token');
  
  // 跳转到 SSO 登出
  window.location.href = 'https://sso.example.com/logout?redirect=' + 
    encodeURIComponent(window.location.origin);
}

关键点

  • 同域 SSO:通过设置主域 Cookie 实现,简单直接
  • 跨域 SSO:需要独立认证中心,通过 ticket 机制传递登录态
  • ticket 一次性:ticket 只能使用一次,用后即废,防止重放攻击
  • 安全考虑:ticket 要有过期时间,传输要用 HTTPS
  • 单点登出:需要 SSO Server 通知所有已登录系统清除会话