Blog

SSO 单点登录入门:从概念到前端回调流程

用一次普通用户登录过程讲清 SSO、回调地址、code、token、会话、权限校验和前后端职责边界。

  • SSO
  • 身份认证
  • 前端
  • 后端
  • 安全

很多系统做到一定规模之后,都会遇到同一个问题:用户不可能在每个系统里都单独注册、单独登录、单独维护密码。

比如一个公司内部可能有这些系统:

OA 系统
项目管理系统
数据看板
知识库
审批系统
第三方业务平台

如果每个系统都自己做登录,用户体验会很差,安全策略也很难统一。SSO 单点登录就是为了解决这个问题。

SSO 是什么

SSO 是 Single Sign-On 的缩写,中文通常叫单点登录。

它的核心目标是:

用户只登录一次,就可以访问多个互相信任的系统。

这里的“单点”不是说系统只有一个页面,而是说登录入口和身份校验能力被收口到了统一身份系统。

用户访问业务系统时,如果还没有登录,业务系统会把用户引导到统一登录平台。用户在统一登录平台完成身份验证后,再回到原来的业务系统。

为什么需要 SSO

SSO 解决的不只是“少输几次密码”。

更重要的是这些工程问题:

  • 统一账号体系:用户、组织、角色从一个地方维护。
  • 统一安全策略:密码规则、多因素认证、登录风控可以集中控制。
  • 统一登录体验:不同业务系统不用各做一套登录页。
  • 降低系统复杂度:业务系统不用直接处理密码。
  • 方便权限扩展:登录后可以继续做角色、订阅、租户等校验。

对于企业系统来说,SSO 往往是身份体系的入口层。

一次 SSO 登录的完整流程

可以先看一个典型流程:

flowchart TD
  user["用户"] --> frontend["业务系统前端"]
  frontend -->|1. 发现用户未登录| login["统一登录平台"]
  login -->|2. 完成账号、密码或其他认证| code["生成一次性 code"]
  code -->|3. 携带 code 回调| backend["业务系统后端"]
  backend -->|4. 用 code 换取用户信息或 token| session["建立业务系统自己的会话"]
  session --> page["用户进入业务页面"]

这个流程里,前端通常负责跳转和展示状态;后端负责真正的 code 校验、token 换取、用户会话建立和权限判断。

回调地址是什么

回调地址是 SSO 流程里非常关键的概念。

业务系统告诉统一登录平台:

用户登录完成后,请把他送回这个地址。

这个地址通常长这样:

https://example.com/login/callback

登录平台完成认证后,会重定向到这个地址,并在 URL 上带一个临时凭证:

https://example.com/login/callback?code=temporary_code

这个 code 一般是短时间有效、只能使用一次的临时凭证。业务系统需要用它去服务端换取用户身份信息。

code 和 token 的区别

很多刚接触 SSO 的人会把 code 和 token 混在一起。可以这样理解:

名称 作用 生命周期
code 登录完成后的临时换票凭证 很短,通常只能用一次
token 代表用户身份或接口访问权限 相对更长,但也应该有过期时间
session 业务系统自己的登录态 由业务系统控制

比较安全的做法是:

flowchart LR
  code["前端拿到 code"] --> exchange["后端用 code 换 token 或用户信息"]
  exchange --> session["后端建立业务系统自己的 session"]
  session --> result["前端只感知“已登录”"]

不要把 client secret、长期 token、第三方接口密钥直接放在前端。

前端在 SSO 中应该做什么

前端适合承担这些职责:

  • 判断当前是否需要跳转登录。
  • 拼接登录地址中的公开参数。
  • 接收回调地址上的 code。
  • 展示“登录中”“校验中”“跳转中”等过渡状态。
  • 把 code 交给后端处理。
  • 根据后端返回结果进入业务页面或显示错误。

前端不适合承担这些职责:

  • 保存 client secret。
  • 直接用 code 调第三方敏感接口。
  • 在浏览器里生成第三方接口签名。
  • 自己判断最终权限。
  • 保存长期敏感 token。

原因很简单:前端代码和构建产物最终都会进入用户浏览器。只要在浏览器里出现过,就不能当成真正的秘密。

后端在 SSO 中应该做什么

后端通常负责更敏感的部分:

  • 校验 code 是否真实有效。
  • 使用 client secret 与统一登录平台通信。
  • 换取用户信息或 access token。
  • 建立业务系统自己的 session。
  • 查询用户角色、组织、租户、订阅状态。
  • 对跳转地址做白名单校验。
  • 记录登录审计日志。

如果系统还要对接第三方服务,最好也由后端完成签名和转发。

推荐结构是:

flowchart LR
  browser["浏览器"] --> backend["业务系统后端"]
  backend --> provider["统一登录平台 / 第三方服务"]

这样可以把密钥、签名、token 和权限判断都收在后端。

SSO 和权限校验不是一回事

SSO 只能证明:

这个用户是谁。

但它不一定能证明:

这个用户能不能访问当前业务。

所以很多系统会在 SSO 登录成功后继续做二次校验,例如:

  • 用户是否属于某个组织。
  • 用户是否拥有某个角色。
  • 用户是否购买或订阅了某个服务。
  • 用户是否在当前租户下有权限。
  • 用户是否被管理员禁用。

这也是为什么一个完整登录流程常常是:

flowchart LR
  authenticate["身份认证"] --> profile["用户信息获取"]
  profile --> authorize["业务权限校验"]
  authorize --> system["进入系统"]

常见异常路径

SSO 集成页一定要把异常路径设计清楚。

常见异常包括:

  • 回调地址没有 code。
  • code 已过期。
  • code 已被使用过。
  • 后端换取用户信息失败。
  • 用户信息缺少唯一 ID。
  • 用户没有访问当前系统的权限。
  • 用户订阅已过期。
  • 登录平台不可用。
  • 跳转地址不在白名单。

这些异常不应该让用户停在空白页。至少应该给出明确提示,并提供一个可控的返回入口。

安全检查清单

做 SSO 集成时,可以用下面这份清单快速自查:

  • client secret 是否只保存在后端。
  • access token 是否避免暴露在 URL 中。
  • code 是否一次性、短有效期。
  • 回调地址是否配置了白名单。
  • 跳转目标是否做了白名单校验。
  • 前端是否避免保存长期敏感 token。
  • 退出登录时是否清理了业务系统 session。
  • 登录失败是否有明确提示和返回入口。
  • 后端是否记录了必要的登录审计。
  • 不同环境是否使用不同的登录配置。

一个简单的前后端分工模板

可以把一次登录拆成这样:

flowchart TD
  subgraph entry["前端入口页"]
    check["检查是否已登录"]
    redirect["未登录则跳转统一登录平台"]
    check --> redirect
  end

  subgraph callback["前端回调页"]
    read["读取 URL 中的 code"]
    requestBackend["调用后端 /auth/sso/callback"]
    status["展示登录状态"]
    read --> requestBackend
    requestBackend --> status
  end

  subgraph backend["后端回调接口"]
    validate["校验 code"]
    exchange["换取用户信息"]
    permission["查询业务权限"]
    session["建立 session"]
    response["返回登录结果"]
    validate --> exchange --> permission --> session --> response
  end

  subgraph business["前端业务页"]
    enter["根据登录结果进入系统"]
    error["失败时显示原因并返回入口"]
  end

  redirect --> read
  requestBackend --> validate
  response --> status
  status --> enter
  status --> error

这个分工的重点是:前端编排流程,后端守住秘密和权限边界。

总结

SSO 单点登录不是简单的“跳到一个登录页再跳回来”。它连接了统一身份、业务系统、权限校验和用户会话,是系统安全边界的一部分。

理解 SSO 时,可以抓住四个关键词:

  • 回调地址:登录完成后回到哪里。
  • code:短期一次性换票凭证。
  • token/session:登录态和接口访问能力。
  • 权限校验:确认用户能不能访问当前业务。

只要把这些边界想清楚,再去实现 Vue、React、后端接口或 Nginx 代理,思路就会顺很多。