在数字化业务日益复杂的今天,用户往往需要在多个子系统(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 中间件。”