🔐单点登录不会?10 行代码带你搭可上线 SSO

121 阅读3分钟

在数字化业务日益复杂的今天,用户往往需要在多个子系统(OA、CRM、BI、移动端 App、小程序)之间来回切换。传统方式要求用户为每个系统单独输入账号密码,既繁琐又易遗忘,还会带来 重复开发、密码泄露、用户体验差 等痛点。
单点登录(Single Sign-On,SSO) 正是为了解决这一问题而诞生:
用户只需在统一认证中心完成一次登录,即可获得访问所有关联系统的权限,无需再次输入凭证。

典型应用场景

  • 集团门户:员工登录一次即可访问邮件、HR、财务、项目协作等内部系统。
  • 电商平台:消费者一次登录即可在 PC 商城、移动 App、小程序之间无缝切换购物车、订单、会员权益。
  • 教育平台:学生一次登录即可进入选课系统、在线课堂、作业提交、成绩查询等子系统。
  • 物联网 SaaS:运维人员一次登录即可同时监控设备、查看报表、下发指令。

接下来,我们将从 底层原理 → 三种主流实现方案 → 完整落地代码 → 运维监控,带你亲手搭建一套可上线的 SSO 系统。


一、SSO 生命周期总览

用户 → 认证中心 → 颁发令牌 → 各子系统 → 令牌续命 → 全局退出
阶段浏览器行为后端行为
首次登录302 跳认证中心校验用户,生成令牌
子系统访问携带令牌网关校验令牌
令牌续命无感刷新刷新令牌有效期
全局退出调用 /logout销毁令牌 & 广播

二、方案一:Cookie + 共享域(最简落地)

1. 域名规划

sso.example.com   # 认证中心
app1.example.com  # 子系统 1
app2.example.com  # 子系统 2

2. 登录中心(Node + Express)

// sso-server.js
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  if (await validateUser(username, password)) {
    // 生成 JWT
    const token = jwt.sign({ sub: username }, process.env.JWT_SECRET, { expiresIn: '2h' });
    // 设置顶级域 Cookie
    res.cookie('sso-token', token, {
      domain: '.example.com',
      httpOnly: true,
      sameSite: 'lax',
      maxAge: 2 * 60 * 60 * 1000
    });
    return res.redirect(req.query.back || 'https://app1.example.com');
  }
  res.status(401).send('Unauthorized');
});

3. 子系统网关校验(Nginx)

location /api/ {
  # 读取 Cookie 中的 sso-token
  auth_jwt "realm";
  auth_jwt_key_file /etc/nginx/jwks.json;
  proxy_pass http://backend;
}

4. 前端验证(微前端)

// 子系统入口
fetch('/api/me', { credentials: 'include' })
  .then(r => r.json())
  .then(data => setUser(data));

三、方案二:OAuth2 + OIDC(跨域/移动端)

1. 授权码流程(标准 4 步)

1. 前端跳转认证中心 → 2. 用户登录 → 3. 返回 code → 4. 前端换 token

2. 后端实现(Spring Authorization Server)

@Configuration
@EnableWebSecurity
public class AuthConfig {
  @Bean
  public RegisteredClientRepository clientRepository() {
    return new InMemoryRegisteredClientRepository(
      RegisteredClient.withId("webApp")
        .clientId("webApp")
        .clientSecret("{noop}secret")
        .redirectUri("https://app.example.com/callback")
        .scope("read", "write")
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .build()
    );
  }
}

3. 前端(React SPA)

// 登录按钮
const login = () => {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'webApp',
    redirect_uri: window.location.origin + '/callback',
    scope: 'read write',
    state: 'xyz'
  });
  location.href = `https://sso.example.com/oauth/authorize?${params}`;
};

// callback 页
import { exchangeCodeForTokens } from 'oauth2-client';
const params = new URLSearchParams(location.search);
const code = params.get('code');
const tokens = await exchangeCodeForTokens(code);
localStorage.setItem('access_token', tokens.access_token);

四、方案三:JWT + API 网关(无状态)

1. 网关统一校验(Kong)

-- Kong 插件
local jwt = require "resty.jwt"
local jwt_obj = jwt:verify(os.getenv("JWT_SECRET"), ngx.var.cookie_sso_token)
if not jwt_obj["verified"] then
  return ngx.exit(401)
end

2. 前端无感刷新(axios 拦截器)

axios.interceptors.response.use(
  res => res,
  async error => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      const { data } = await axios.post('/auth/refresh', {}, { withCredentials: true });
      axios.defaults.headers.common['Authorization'] = `Bearer ${data.access_token}`;
      return axios(originalRequest);
    }
    return Promise.reject(error);
  }
);

五、高级特性

1. 全局退出广播

// 认证中心
app.post('/logout', (req, res) => {
  res.clearCookie('sso-token', { domain: '.example.com' });
  // 向各子系统发送 WebSocket 广播
  io.emit('logout', req.user.id);
  res.sendStatus(200);
});

2. 权限校验(RBAC)

// 网关中间件
const canAccess = (userId, resource) => {
  const roles = await redis.sMembers(`roles:${userId}`);
  return roles.includes(resource);
};

六、部署清单

组件推荐配置
认证中心Node.js + Redis + JWT
网关Kong / Nginx + Lua
监控Prometheus + Grafana(令牌过期率、登录失败率)

七、面试 30 秒回答模板

“我做过 Cookie + JWT 两套 SSO:

  • Cookie 方案:顶级域 Cookie + Nginx auth_jwt,10 行配置搞定内部系统。
  • OAuth2 方案:授权码 + 网关统一鉴权,支持跨域和移动端。
  • 全局退出用 WebSocket 广播,权限用 RBAC 中间件。”