单点登录(SSO)这玩意儿,很多人第一次接触是在公司内网:你开了 OA,再点文档系统,不用再输密码;再点工单系统,还是不用。

这时候你会产生一种错觉:好像“全公司共用一个账号密码”就完事了。

其实不是。SSO 的精髓不是“共用密码”,而是“共用一次登录的结果”,也就是:我在一个可信的地方证明过“我是谁”,其他系统就别再折腾我了。

先一句话:什么是单点登录?

单点登录(Single Sign-On, SSO):用户在一个统一的认证中心登录一次之后,就能访问多个相互信任的系统/应用,而不需要在每个系统里重复登录。

它解决的主要是两件事:

  • 用户少输几次账号密码(体验)
  • 公司少养几套“用户体系 + 权限体系 + 登录逻辑”(治理)

先认清角色:谁负责“认人”,谁负责“用人”?

SSO 场景里最常见有两个角色:

  • IdP(Identity Provider,身份提供方):负责认证,也就是“我来确认你是谁”。常见是统一登录中心。
  • SP(Service Provider,服务提供方):具体业务系统,比如 OA、工单、文档、财务系统等。

你可以把它理解成:

  • IdP = 门禁闸机
  • SP = 各个办公室

办公室只关心“你有没有门禁权限”,不想天天自己发卡、自己验指纹。

一个最常见的 SSO 流程(浏览器跳转版)

以“你打开工单系统”举例,最典型的流程是“重定向来回跳”:

  1. 你访问工单系统(SP)
  2. SP 发现你没登录,于是把你 302 到登录中心(IdP)
  3. 你在 IdP 登录成功(可能是账号密码/短信/扫码/企业微信)
  4. IdP 给你一个“证明”(ticket / code / assertion / token 之类的东西),再把你 302 回 SP
  5. SP 拿着这个“证明”去 IdP 验一下真伪,验过后给你建本地会话(session/cookie),你就进去了

这套过程你肉眼看到的就是:地址栏闪了几下,最后回到了业务系统,登录状态还在。

流程图(把“跳来跳去”画出来)

示例代码(JavaScript):SP 侧最常见的两段(拦截 + 回调)

下面用“接近业务代码但不绑定具体实现细节”的 JavaScript 示例写一下,核心就是两段:没登录就跳 IdP;回调里拿 code 去换 token,然后给自己发 cookie。

1) 业务接口/页面的登录拦截(Auth Guard)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import crypto from 'node:crypto';

const SP_CLIENT_ID = process.env.SP_CLIENT_ID;
const SP_CALLBACK_URL = process.env.SP_CALLBACK_URL;
const IDP_AUTHORIZE_ENDPOINT = process.env.IDP_AUTHORIZE_ENDPOINT;

const tempStates = new Map();

function createState() {
return crypto.randomBytes(16).toString('hex');
}

function saveTempState(state, meta, ttlMs = 5 * 60 * 1000) {
const expireAt = Date.now() + ttlMs;
tempStates.set(state, { meta, expireAt });
}

function buildAuthorizeUrl({ state }) {
const url = new URL(IDP_AUTHORIZE_ENDPOINT);
url.searchParams.set('client_id', SP_CLIENT_ID);
url.searchParams.set('redirect_uri', SP_CALLBACK_URL);
url.searchParams.set('response_type', 'code');
url.searchParams.set('scope', 'openid profile');
url.searchParams.set('state', state);
return url.toString();
}

export function authGuard(req, res, next) {
const sessionId = req.cookies?.sp_session;
if (sessionId && isValidSession(sessionId)) {
next();
return;
}

const state = createState();
saveTempState(state, { ua: req.headers['user-agent'], ip: req.ip });
res.redirect(302, buildAuthorizeUrl({ state }));
}

2) IdP 回调处理(用 code 换登录态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const SP_CLIENT_SECRET = process.env.SP_CLIENT_SECRET;
const IDP_TOKEN_ENDPOINT = process.env.IDP_TOKEN_ENDPOINT;

function verifyTempState(state) {
const item = tempStates.get(state);
if (!item) return false;
if (item.expireAt < Date.now()) {
tempStates.delete(state);
return false;
}
tempStates.delete(state);
return true;
}

async function exchangeCodeForToken({ code }) {
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: SP_CLIENT_ID,
client_secret: SP_CLIENT_SECRET,
code,
redirect_uri: SP_CALLBACK_URL,
});

const resp = await fetch(IDP_TOKEN_ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});

if (!resp.ok) throw new Error(`token exchange failed: ${resp.status}`);
return resp.json();
}

export async function handleCallback(req, res) {
const { code, state } = req.query || {};
if (!code || !state || !verifyTempState(String(state))) {
res.status(400).send('invalid state');
return;
}

const token = await exchangeCodeForToken({ code: String(code) });
const user = verifyIdTokenAndGetUser(token.id_token);
const sessionId = createSession({ userId: user.id, expiresInSeconds: 8 * 60 * 60 });

res.cookie('sp_session', sessionId, { httpOnly: true, secure: true, sameSite: 'lax' });
res.redirect(302, '/app');
}

那个“证明”到底是什么?

这就是 SSO 里最容易把人绕晕的地方:不同体系,名字不一样,但核心是一样的:

  • 它能证明“用户已在 IdP 登录过”
  • 它最好是一次性的/短期有效的
  • SP 可以拿它去 IdP 校验,或者它本身可校验(带签名)

常见几种主流方案你大概听过:

1) SAML(企业内网很常见)

  • IdP 发给 SP 一个 SAML Assertion(通常是 XML)
  • Assertion 里包含用户信息、有效期、签名等
  • SP 校验签名,确认“确实是 IdP 发的”

优点:企业生态成熟;缺点:XML + 各种配置,工程体验相对“硬核”。

2) OAuth 2.0 / OIDC(现代 Web 更常见)

这里容易混:

  • OAuth 2.0:更偏“授权”(允许 A 系统代表你去访问 B 系统资源)
  • OIDC(OpenID Connect):在 OAuth 2.0 基础上补了“身份认证”,更适合“登录”

常见落地是 Authorization Code Flow:

  1. SP 把你跳到 IdP,让你登录并授权
  2. IdP 回给 SP 一个 code
  3. SP 用 codeid_token(身份)和 access_token(资源访问)

“单点”到底单在哪?别误会了

SSO 的“单点”不是说:

  • 所有系统只有一份 cookie
  • 所有系统都共享同一个 session

更真实的情况是:

  • IdP 有自己的登录态(一般是 IdP 域名下的 cookie/session)
  • 每个 SP 也会建自己的登录态(SP 域名下的 cookie/session)

SSO 做的事情是:当某个 SP 没登录时,它可以“借助 IdP 的登录态”快速把自己也登录上。

所以你会看到一个很合理的现象:

  • 你清了某个业务系统的 cookie,它会要求你登录
  • 但它又会立刻把你跳到 IdP,然后瞬间把你跳回来(因为 IdP 还记得你)

这就是“单点登录”的真实运行方式。

你以为最难的是“登录”,其实是“登出”

登录能统一,登出就麻烦了:你到底要退出哪儿?

1) 单点登出(SLO)

理想情况:你在任何一个系统点退出,所有系统都跟着退出。

实现成本:偏高。因为你要让各个 SP 配合,挨个通知、挨个清会话,还要处理“有系统挂了/没响应”的情况。

2) 更常见的现实做法

很多公司做的是“半单点登出”:

  • 退出当前系统(清这个 SP 的 cookie)
  • 退出登录中心(清 IdP 的 cookie)

这样下次再访问其他系统,它们需要重新走一遍 SSO 流程才能登录上(因为 IdP 已经不认你了)。

前端最常见的坑(别问,问就是踩过)

现代浏览器对第三方 cookie 越来越严格,尤其是 IdP 和 SP 不同域名时,跳转链路里 cookie 是否能带上,会受 SameSite 影响。

你会遇到一种特别折磨的现象:你明明登录成功了,但回到业务系统又说你没登录,然后又跳回登录中心,如此循环。

这类问题通常要从:

  • cookie 的 SameSite 配置
  • 是否 HTTPS(Secure
  • 是否是 iframe 场景(更麻烦)

这几个方向去查。

额外补一张:单点登出(SLO)大概怎么“通知全家”

登出这个话题比较“现实主义”:你想一键退出所有系统,就要把“通知链路”也搭起来。

2) 把 token 存 localStorage,然后被 XSS 一锅端

如果你用的是 OIDC/OAuth 的 token 体系,前端总有人想图省事:

  • access_token 存 localStorage

然后你只要有一次 XSS,就相当于把门禁卡递给了攻击者。

更稳的方式通常是:

  • 敏感 token 尽量别落到 JS 可读的存储里
  • 或者把关键会话交给后端维护(BFF 模式),前端只拿 httpOnly cookie

3) 回调地址配置得像散装的

SSO 基本都依赖回调地址(redirect_uri / ACS URL)。配置一旦散装:

  • 多环境(dev/staging/prod)经常漏配
  • 前端换域名/换路径,登录就全炸

建议从第一天就把回调地址治理成“可配置、可管理、可审计”的资产,别让它变成“谁改谁背锅”的黑盒。

怎么判断你们到底需不需要 SSO?

如果符合这些情况,SSO 基本就值得上:

  • 你们有多个系统,用户需要频繁切换
  • 用户体系重复建设严重(每个系统都维护一份用户/登录/密码策略)
  • 安全要求高(MFA、统一风控、统一审计、统一封禁)

反过来,如果就一个系统,或者系统之间不怎么互相访问,上 SSO 可能是“为了解决不存在的问题”。


SSO 这事儿说到底就一句话:把“认证”从业务系统里抽出来,交给专业的认证中心做。业务系统别天天纠结你是谁,它应该更关心:你能不能干这件事(授权/权限)。

Happy Coding!