单点登录方案(SSO)
1. 一句话概括主题
单点登录(SSO)是一种让用户只需登录一次,就能访问多个相关应用系统的认证方案。
2. 它是什么
想象一下,你每天要登录公司的邮箱、OA系统、项目管理工具、财务系统等十多个系统。如果每个系统都要单独登录,每次都要输入用户名密码,是不是很烦?
单点登录就像是一把”万能钥匙”:你只需要在第一个系统登录一次,之后访问其他系统时,系统会自动识别你已经登录过了,直接放行,不用再输入密码。
打个比方:
- 传统方式:就像进一栋大楼,每层楼都要刷卡,很麻烦
- SSO方式:就像进一栋大楼,在大厅刷一次卡,之后所有楼层都能直接进
举个例子:
- 你登录了 Google 账号,然后访问 Gmail、YouTube、Google Drive 时都不需要再登录
- 企业内网中,登录了统一认证系统后,访问各个业务系统都自动登录
3. 能解决什么问题 + 为什么重要
解决的痛点
- 用户体验差:用户需要记住多套账号密码,频繁登录很麻烦
- 管理成本高:IT部门需要为每个系统单独管理用户账号
- 安全风险大:用户可能因为密码太多而使用弱密码,或者写在纸上
- 数据孤岛:各系统用户信息不统一,难以做统一的数据分析
为什么重要
- 提升用户体验:一次登录,处处通行,用户满意度大幅提升
- 降低管理成本:统一管理用户账号,减少运维工作量
- 提高安全性:集中管理认证,更容易实施安全策略(如密码复杂度、双因素认证)
- 便于扩展:新增系统时,只需接入SSO,无需重新开发认证模块
4. 核心知识点拆解
基本概念
单点登录(SSO):Single Sign-On,用户在一个系统登录后,可以访问其他信任的系统而无需再次登录。
认证中心(IdP):Identity Provider,负责用户身份认证的中心系统,比如统一登录页面。
服务提供方(SP):Service Provider,需要认证的各个业务系统,比如OA系统、财务系统。
信任关系:SP信任IdP的认证结果,IdP信任SP是合法的。
必会概念与关键字
1. Token(令牌)
认证中心颁发的凭证,证明用户已登录。
// Token 通常包含用户信息、过期时间等
{
userId: "12345",
username: "zhangsan",
expireTime: "2025-01-20 12:00:00",
signature: "加密签名"
}1.1. AccessToken(访问令牌)
用于访问受保护资源的短期令牌,通常有效期较短(如 2 小时)。
特点:
- 有效期短,提高安全性
- 每次 API 请求都需要携带
- 过期后需要刷新
// 登录成功后返回的 AccessToken
{
"accessToken": "a641a16b76e44501ba9ab2bfcea3d469",
"expiresIn": 7200, // 2小时后过期(单位:秒)
"tokenType": "Bearer"
}
// 使用 AccessToken 访问 API
fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});1.2. RefreshToken(刷新令牌)
用于刷新 AccessToken 的长期令牌,通常有效期较长(如 7 天或 30 天)。
特点:
- 有效期长,用于获取新的 AccessToken
- 不直接用于访问资源,只用于刷新
- 需要安全存储(如 HttpOnly Cookie)
// 登录成功后返回的 RefreshToken
{
"accessToken": "a641a16b76e44501ba9ab2bfcea3d469",
"refreshToken": "21ee0ad85b2946fa91c16f893c97345c",
"expiresIn": 7200,
"refreshExpiresIn": 604800 // 7天后过期
}
// 使用 RefreshToken 刷新 AccessToken
async function refreshAccessToken() {
const response = await fetch('https://sso.example.com/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: localStorage.getItem('refreshToken')
})
});
const data = await response.json();
// 更新 AccessToken
localStorage.setItem('accessToken', data.accessToken);
// RefreshToken 可能会更新,也可能不变
if (data.refreshToken) {
localStorage.setItem('refreshToken', data.refreshToken);
}
return data.accessToken;
}AccessToken 和 RefreshToken 配合使用:
// 完整的 Token 管理示例
class TokenManager {
constructor() {
this.accessToken = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
}
// 检查 AccessToken 是否过期
isAccessTokenExpired() {
if (!this.accessToken) return true;
// 实际应该解析 JWT 或检查服务器返回的过期时间
const tokenData = JSON.parse(atob(this.accessToken.split('.')[1]));
return Date.now() >= tokenData.exp * 1000;
}
// 获取有效的 AccessToken(自动刷新)
async getValidAccessToken() {
// 如果 AccessToken 未过期,直接返回
if (!this.isAccessTokenExpired()) {
return this.accessToken;
}
// AccessToken 过期,使用 RefreshToken 刷新
if (this.refreshToken) {
try {
const newToken = await this.refreshAccessToken();
this.accessToken = newToken;
localStorage.setItem('accessToken', newToken);
return newToken;
} catch (error) {
// RefreshToken 也过期了,需要重新登录
this.clearTokens();
window.location.href = '/login';
throw error;
}
} else {
// 没有 RefreshToken,需要重新登录
window.location.href = '/login';
throw new Error('No refresh token available');
}
}
// 刷新 AccessToken
async refreshAccessToken() {
const response = await fetch('https://sso.example.com/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
if (!response.ok) {
throw new Error('Failed to refresh token');
}
const data = await response.json();
return data.accessToken;
}
// 清除所有 Token
clearTokens() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
this.accessToken = null;
this.refreshToken = null;
}
}
// 使用示例
const tokenManager = new TokenManager();
// 在 API 请求中使用
async function fetchUserData() {
const accessToken = await tokenManager.getValidAccessToken();
const response = await fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}2. Session(会话)
服务器端保存的用户登录状态。
// 服务器端 Session 存储
sessionStorage.setItem('sso_token', token);
sessionStorage.setItem('user_info', userInfo);3. Cookie 共享
同一域名下的 Cookie 可以共享,这是实现 SSO 的基础。详见 Cookie-API - Domain(域名)。
// 设置 Cookie 在父域名下,子域名可以共享
document.cookie = "sso_token=xxx; domain=.example.com; path=/";关键点:
domain=.example.com前面必须加.(点),表示所有子域名都能访问path=/必须设置为根路径,才能全站可见- 详细说明请参考 Cookie-API - 为什么两个站点的 Cookie 不共享?
4. 重定向(Redirect)
用户访问系统时,如果未登录,重定向到认证中心。
// 未登录时重定向到认证中心
if (!isLoggedIn) {
window.location.href = 'https://sso.example.com/login?redirect=' + encodeURIComponent(currentUrl);
}关联知识
- Token → JWT 认证实现
- Session → Session 管理
- Cookie → Cookie-API — Cookie 完整知识体系(Domain、Path、Secure、SameSite)
- Cookie-API - Domain(域名) — 跨子域 Cookie 共享的核心
- Cookie-API - 为什么两个站点的 Cookie 不共享? — 理解 Cookie 共享原理
- Cookie-API - 如何在控制台正确设置跨子域 Cookie? — 实践指南
- Cookie-API - 场景2:单点登录(SSO)与跨子域 Cookie 共享 — SSO 实际应用
- Cookie → HTTP 基础
- 重定向 → HTTP 状态码
- 加密签名 → 加密算法
常见误解说明与纠正
误解1:SSO 就是共享 Cookie
- 纠正:Cookie 共享只是实现方式之一,还有 Token、CAS 协议等多种方式
误解2:所有系统必须用同一个域名
- 纠正:跨域 SSO 可以通过 Token、OAuth 2.0 等方式实现
误解3:SSO 不安全,一个账号被破解所有系统都危险
- 纠正:SSO 集中管理反而更容易实施安全策略,如双因素认证、异常检测等
5. 示例代码(可运行 + 逐行注释)
方案一:同域 Cookie 共享(最简单)
💡 相关知识点:详细原理请参考 Cookie-API - 如何在控制台正确设置跨子域 Cookie?
// SSO 认证中心登录成功后,设置 Cookie
// 在认证中心(sso.example.com)
function loginSuccess(userInfo) {
// 生成 Token
const token = generateToken(userInfo);
// 设置 Cookie 到父域名,所有子域名都能访问
// domain=.example.com 表示 example.com 及其所有子域名都能访问
// 注意:domain 前面必须加 .(点),详见 [Cookie-API - Domain(域名)](../../02-编程语言/01-JavaScript/08-浏览器环境与DOM/13-Cookie-API.md#domain域名)
document.cookie = `sso_token=${token}; domain=.example.com; path=/; max-age=3600`;
// 跳转回原系统
const redirectUrl = getUrlParam('redirect') || 'https://oa.example.com';
window.location.href = redirectUrl;
}
// 业务系统(oa.example.com)检查登录状态
function checkLogin() {
// 读取 Cookie 中的 Token
const token = getCookie('sso_token');
if (!token) {
// 没有 Token,跳转到认证中心登录
const currentUrl = window.location.href;
window.location.href = `https://sso.example.com/login?redirect=${encodeURIComponent(currentUrl)}`;
return;
}
// 验证 Token 是否有效
validateToken(token).then(valid => {
if (!valid) {
// Token 无效,清除 Cookie 并跳转登录
document.cookie = 'sso_token=; domain=.example.com; path=/; max-age=0';
window.location.href = 'https://sso.example.com/login?redirect=' + encodeURIComponent(window.location.href);
} else {
// Token 有效,获取用户信息
getUserInfo(token).then(userInfo => {
console.log('用户已登录:', userInfo);
});
}
});
}
// 辅助函数:获取 Cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// 辅助函数:验证 Token(实际应该调用后端接口)
function validateToken(token) {
return fetch('https://sso.example.com/api/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
}).then(res => res.json()).then(data => data.valid);
}
// 页面加载时检查登录状态
checkLogin();方案二:跨域 Token 传递(适用于不同域名)
// 认证中心登录成功后,通过 URL 参数传递 Token
// 在认证中心(sso.example.com)
function loginSuccess(userInfo) {
const token = generateToken(userInfo);
const redirectUrl = getUrlParam('redirect');
// 将 Token 作为参数传递到业务系统
window.location.href = `${redirectUrl}?sso_token=${token}`;
}
// 业务系统(app.otherdomain.com)接收 Token
function handleSSOToken() {
// 从 URL 参数中获取 Token
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('sso_token');
if (token) {
// 验证 Token
validateToken(token).then(valid => {
if (valid) {
// Token 有效,保存到本地存储
localStorage.setItem('sso_token', token);
// 清除 URL 中的 Token 参数(安全考虑)
window.history.replaceState({}, document.title, window.location.pathname);
// 获取用户信息
getUserInfo(token).then(userInfo => {
console.log('用户已登录:', userInfo);
});
} else {
// Token 无效,跳转登录
redirectToLogin();
}
});
} else {
// 没有 Token,检查本地存储
const savedToken = localStorage.getItem('sso_token');
if (savedToken) {
validateToken(savedToken).then(valid => {
if (!valid) {
localStorage.removeItem('sso_token');
redirectToLogin();
}
});
} else {
redirectToLogin();
}
}
}
// 跳转到认证中心登录
function redirectToLogin() {
const currentUrl = window.location.href;
window.location.href = `https://sso.example.com/login?redirect=${encodeURIComponent(currentUrl)}`;
}
// 页面加载时处理 SSO Token
handleSSOToken();方案三:CAS 协议实现(标准方案)
// CAS(Central Authentication Service)是标准的 SSO 协议
// 业务系统检查登录状态
function checkCASLogin() {
// 检查是否有 CAS Ticket(临时票据)
const urlParams = new URLSearchParams(window.location.search);
const ticket = urlParams.get('ticket');
if (ticket) {
// 有 Ticket,向认证中心验证
validateCASTicket(ticket).then(result => {
if (result.valid) {
// Ticket 有效,保存用户信息
localStorage.setItem('cas_user', JSON.stringify(result.user));
localStorage.setItem('cas_token', result.token);
// 清除 URL 中的 Ticket
window.history.replaceState({}, document.title, window.location.pathname);
} else {
redirectToCASLogin();
}
});
} else {
// 没有 Ticket,检查本地是否有登录信息
const casUser = localStorage.getItem('cas_user');
if (!casUser) {
redirectToCASLogin();
}
}
}
// 验证 CAS Ticket
function validateCASTicket(ticket) {
const serviceUrl = window.location.origin + window.location.pathname;
return fetch(`https://sso.example.com/cas/validate?ticket=${ticket}&service=${encodeURIComponent(serviceUrl)}`, {
method: 'GET',
credentials: 'include'
}).then(res => res.json());
}
// 跳转到 CAS 登录页面
function redirectToCASLogin() {
const serviceUrl = window.location.href;
window.location.href = `https://sso.example.com/cas/login?service=${encodeURIComponent(serviceUrl)}`;
}
// 登出
function casLogout() {
localStorage.removeItem('cas_user');
localStorage.removeItem('cas_token');
window.location.href = 'https://sso.example.com/cas/logout';
}
// 页面加载时检查
checkCASLogin();方案四:AccessToken + RefreshToken 实现(推荐方案)
// 使用 AccessToken 和 RefreshToken 实现 SSO
// 这是目前最推荐的方式,兼顾安全性和用户体验
// ========== 登录流程 ==========
// 在认证中心(sso.example.com)登录成功后
async function loginSuccess(username, password) {
// 调用登录接口
const response = await fetch('https://sso.example.com/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
// 返回的数据格式:
// {
// "accessToken": "a641a16b76e44501ba9ab2bfcea3d469",
// "refreshToken": "21ee0ad85b2946fa91c16f893c97345c",
// "expiresIn": 7200, // AccessToken 2小时后过期
// "refreshExpiresIn": 604800 // RefreshToken 7天后过期
// }
// 保存 Token 到本地存储
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
localStorage.setItem('tokenExpireTime', Date.now() + data.expiresIn * 1000);
// 跳转回业务系统
const redirectUrl = getUrlParam('redirect') || 'https://app.example.com';
window.location.href = redirectUrl;
}
// ========== Token 管理器 ==========
class SSOTokenManager {
constructor() {
this.accessToken = localStorage.getItem('accessToken');
this.refreshToken = localStorage.getItem('refreshToken');
this.tokenExpireTime = parseInt(localStorage.getItem('tokenExpireTime') || '0');
}
// 检查 AccessToken 是否过期(提前5分钟刷新)
isAccessTokenExpired() {
if (!this.accessToken) return true;
// 提前5分钟刷新,避免在请求过程中过期
return Date.now() >= (this.tokenExpireTime - 5 * 60 * 1000);
}
// 获取有效的 AccessToken(自动刷新)
async getValidAccessToken() {
// 如果 AccessToken 未过期,直接返回
if (!this.isAccessTokenExpired()) {
return this.accessToken;
}
// AccessToken 过期,使用 RefreshToken 刷新
if (this.refreshToken) {
try {
console.log('AccessToken 已过期,正在刷新...');
const newTokenData = await this.refreshAccessToken();
// 更新本地存储
this.accessToken = newTokenData.accessToken;
this.tokenExpireTime = Date.now() + newTokenData.expiresIn * 1000;
localStorage.setItem('accessToken', newTokenData.accessToken);
localStorage.setItem('tokenExpireTime', this.tokenExpireTime.toString());
// RefreshToken 可能会更新
if (newTokenData.refreshToken) {
this.refreshToken = newTokenData.refreshToken;
localStorage.setItem('refreshToken', newTokenData.refreshToken);
}
return this.accessToken;
} catch (error) {
console.error('刷新 Token 失败:', error);
// RefreshToken 也过期了,需要重新登录
this.clearTokens();
this.redirectToLogin();
throw error;
}
} else {
// 没有 RefreshToken,需要重新登录
this.redirectToLogin();
throw new Error('No refresh token available');
}
}
// 刷新 AccessToken
async refreshAccessToken() {
const response = await fetch('https://sso.example.com/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: this.refreshToken
})
});
if (!response.ok) {
// 如果返回 401,说明 RefreshToken 也过期了
if (response.status === 401) {
throw new Error('RefreshToken expired');
}
throw new Error('Failed to refresh token');
}
const data = await response.json();
// 返回格式:
// {
// "accessToken": "新的accessToken",
// "refreshToken": "新的refreshToken(可选)",
// "expiresIn": 7200
// }
return data;
}
// 清除所有 Token
clearTokens() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('tokenExpireTime');
this.accessToken = null;
this.refreshToken = null;
this.tokenExpireTime = 0;
}
// 跳转到登录页
redirectToLogin() {
const currentUrl = window.location.href;
window.location.href = `https://sso.example.com/login?redirect=${encodeURIComponent(currentUrl)}`;
}
}
// ========== 业务系统使用 ==========
// 在业务系统(app.example.com)中使用
// 创建全局 Token 管理器实例
const tokenManager = new SSOTokenManager();
// 封装带自动刷新的 fetch 请求
async function authenticatedFetch(url, options = {}) {
// 获取有效的 AccessToken(自动刷新)
const accessToken = await tokenManager.getValidAccessToken();
// 在请求头中添加 Authorization
const headers = {
...options.headers,
'Authorization': `Bearer ${accessToken}`
};
// 发起请求
const response = await fetch(url, {
...options,
headers
});
// 如果返回 401,可能是 Token 无效,尝试刷新一次
if (response.status === 401) {
console.log('收到 401,尝试刷新 Token...');
try {
const newToken = await tokenManager.getValidAccessToken();
// 用新 Token 重试请求
headers['Authorization'] = `Bearer ${newToken}`;
return fetch(url, {
...options,
headers
});
} catch (error) {
// 刷新失败,跳转登录
tokenManager.redirectToLogin();
throw error;
}
}
return response;
}
// 使用示例:获取用户信息
async function getUserProfile() {
try {
const response = await authenticatedFetch('https://api.example.com/user/profile');
const userInfo = await response.json();
console.log('用户信息:', userInfo);
return userInfo;
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
// 使用示例:调用其他 API
async function fetchUserOrders() {
try {
const response = await authenticatedFetch('https://api.example.com/orders', {
method: 'GET'
});
const orders = await response.json();
return orders;
} catch (error) {
console.error('获取订单失败:', error);
}
}
// ========== 页面初始化 ==========
// 页面加载时检查登录状态
async function initApp() {
// 检查是否有 Token
if (!tokenManager.accessToken) {
// 没有 Token,跳转到登录页
tokenManager.redirectToLogin();
return;
}
// 验证 Token 是否有效(尝试获取用户信息)
try {
const userInfo = await getUserProfile();
console.log('用户已登录:', userInfo);
// 继续初始化应用
} catch (error) {
console.error('Token 验证失败:', error);
// Token 无效,清除并跳转登录
tokenManager.clearTokens();
tokenManager.redirectToLogin();
}
}
// 页面加载时初始化
initApp();
// ========== 登出功能 ==========
async function logout() {
try {
// 调用登出接口(可选,用于服务端清除 RefreshToken)
const accessToken = tokenManager.accessToken;
if (accessToken) {
await fetch('https://sso.example.com/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
}
} catch (error) {
console.error('登出请求失败:', error);
} finally {
// 清除本地 Token
tokenManager.clearTokens();
// 跳转到登录页
tokenManager.redirectToLogin();
}
}6. 常见错误与踩坑
错误1:Cookie 域名设置错误,导致子域名无法共享
为什么错:Cookie 的 domain 属性设置不正确,导致子域名无法读取。详见 Cookie-API - 错误1:Domain 设置错误,导致子域名无法共享。
会导致什么:用户在认证中心登录后,业务系统仍然提示未登录。
正确方式:
// ❌ 错误:只设置当前域名
document.cookie = "sso_token=xxx; domain=sso.example.com";
// ✅ 正确:设置父域名,所有子域名都能访问
// 注意:domain 前面必须加 .(点),详见 [Cookie-API - Domain 的三种情况详解](../../02-编程语言/01-JavaScript/08-浏览器环境与DOM/13-Cookie-API.md#domain-的三种情况详解)
document.cookie = "sso_token=xxx; domain=.example.com; path=/";更多示例:参考 Cookie-API - 常见错误示例 了解完整的错误场景和解决方案。
错误2:Token 通过 URL 传递但没有及时清除,存在安全风险
为什么容易踩:开发时为了方便,Token 通过 URL 参数传递,但忘记清除,导致 Token 泄露。
正确方式:
// ❌ 错误:Token 一直留在 URL 中
window.location.href = `https://app.com?token=${token}`;
// 用户可能分享这个 URL,导致 Token 泄露
// ✅ 正确:获取 Token 后立即清除 URL 参数
const token = new URLSearchParams(window.location.search).get('token');
if (token) {
localStorage.setItem('token', token);
// 清除 URL 中的 Token
window.history.replaceState({}, document.title, window.location.pathname);
}错误3:没有处理 Token 过期,用户突然被登出
真实开发场景:用户正在填写表单,突然 Token 过期,页面跳转到登录页,用户输入的数据丢失。
正确方式:
// ✅ 正确:在请求拦截器中处理 Token 过期
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Token 过期或无效
const currentUrl = window.location.href;
// 保存当前页面状态(如表单数据)
sessionStorage.setItem('form_data', JSON.stringify(formData));
// 跳转登录,登录后返回原页面
window.location.href = `https://sso.example.com/login?redirect=${encodeURIComponent(currentUrl)}`;
}
return Promise.reject(error);
}
);错误4:将 RefreshToken 存储在 localStorage,存在 XSS 风险
为什么错:RefreshToken 有效期长,如果存储在 localStorage,容易被 XSS 攻击窃取。
会导致什么:攻击者获取 RefreshToken 后,可以长期使用,危害更大。
正确方式:
// ❌ 错误:RefreshToken 存储在 localStorage(容易被 XSS 窃取)
localStorage.setItem('refreshToken', refreshToken);
// ✅ 正确:RefreshToken 存储在 HttpOnly Cookie 中(服务端设置)
// 前端无法通过 JavaScript 访问,防止 XSS 攻击
// 服务端设置 Cookie:
// Set-Cookie: refreshToken=xxx; HttpOnly; Secure; SameSite=Strict
// 或者使用更安全的方式:RefreshToken 也设置较短的过期时间,并配合设备指纹错误5:AccessToken 过期后没有自动刷新,导致频繁登录
为什么容易踩:每次请求都检查 Token 是否过期,但没有实现自动刷新机制。
正确方式:
// ❌ 错误:只检查过期,不自动刷新
function makeRequest() {
const token = localStorage.getItem('accessToken');
if (isTokenExpired(token)) {
// 直接跳转登录,用户体验差
window.location.href = '/login';
return;
}
// 发起请求...
}
// ✅ 正确:自动刷新 Token
async function makeRequest() {
const tokenManager = new SSOTokenManager();
// 自动获取有效 Token(过期会自动刷新)
const validToken = await tokenManager.getValidAccessToken();
// 使用有效 Token 发起请求
fetch(url, {
headers: { 'Authorization': `Bearer ${validToken}` }
});
}错误6:多个并发请求同时刷新 Token,导致重复刷新
真实开发场景:页面同时发起多个 API 请求,每个请求都检测到 Token 过期,都去刷新,导致重复刷新。
正确方式:
// ✅ 正确:使用 Promise 缓存,确保同时只有一个刷新请求
class SSOTokenManager {
constructor() {
this.refreshPromise = null; // 缓存刷新 Promise
}
async getValidAccessToken() {
if (!this.isAccessTokenExpired()) {
return this.accessToken;
}
// 如果正在刷新,直接返回缓存的 Promise
if (this.refreshPromise) {
const result = await this.refreshPromise;
return result.accessToken;
}
// 开始刷新,缓存 Promise
this.refreshPromise = this.refreshAccessToken();
try {
const result = await this.refreshPromise;
this.accessToken = result.accessToken;
return result.accessToken;
} finally {
// 刷新完成后清除缓存
this.refreshPromise = null;
}
}
}7. 实际应用场景
场景1:企业内部系统统一登录
需求:公司有OA系统、财务系统、人事系统等多个系统,希望员工只需登录一次。
解决方案:
- 搭建统一认证中心(SSO服务器)
- 各业务系统接入SSO
- 用户访问任何系统时,未登录则跳转到认证中心
- 登录成功后,各系统自动识别已登录状态
场景2:第三方登录(OAuth 2.0)
需求:网站允许用户使用微信、QQ、GitHub等账号登录。
解决方案:
- 使用 OAuth 2.0 协议
- 用户点击”微信登录”,跳转到微信授权页面
- 用户授权后,微信返回授权码
- 网站用授权码换取 Access Token
- 用 Token 获取用户信息,完成登录
场景3:微服务架构中的统一认证
需求:微服务架构中,多个服务需要共享用户认证状态。
解决方案:
- 使用 JWT Token
- 认证服务生成 Token,包含用户信息和签名
- 各微服务验证 Token 签名,无需调用认证服务
- Token 可以设置过期时间,提高安全性
8. 给新手的练习题(可立即实践)
基础题:实现同域 Cookie 共享的简单 SSO
目标:创建两个页面(认证中心和业务系统),实现简单的 SSO 登录。
要求:
- 认证中心页面:输入用户名密码(可以写死),登录成功后设置 Cookie
- 业务系统页面:检查 Cookie,如果没有则跳转到认证中心
- 登录成功后显示用户信息
输出结果:
- 在认证中心登录后,访问业务系统自动显示已登录状态
- 清除 Cookie 后,访问业务系统自动跳转到认证中心
提示代码结构:
<!-- sso.html (认证中心) -->
<!DOCTYPE html>
<html>
<head>
<title>SSO 认证中心</title>
</head>
<body>
<h1>登录</h1>
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button onclick="login()">登录</button>
<script>
function login() {
const username = document.getElementById('username').value;
// 简单验证(实际应该调用后端接口)
if (username) {
// 设置 Cookie
document.cookie = `sso_user=${username}; domain=.localhost; path=/`;
// 跳转回业务系统
const redirect = new URLSearchParams(window.location.search).get('redirect') || 'http://app.localhost:8080';
window.location.href = redirect;
}
}
</script>
</body>
</html>进阶题:实现跨域 Token 传递的 SSO
目标:实现两个不同域名的系统之间的 SSO。
要求:
- 认证中心在
sso.example.com - 业务系统在
app.otherdomain.com - 使用 Token 传递方式实现 SSO
- Token 需要验证签名,防止伪造
输出结果:
- 跨域环境下实现 SSO 登录
- Token 验证机制正常工作
- 安全处理 Token 传递
9. 用更简单的话再总结一遍(方便复习)
单点登录就是”一次登录,处处通行”:
- 你在认证中心登录一次
- 之后访问其他系统时,系统自动识别你已经登录了
- 不用再输入密码,直接就能用
实现方式:
- 同域名:用 Cookie 共享,最简单
- 跨域名:用 Token 传递,通过 URL 或 PostMessage
- 标准方案:用 CAS 协议或 OAuth 2.0,更安全可靠
记住三点:
- 认证中心负责登录,业务系统负责验证
- Token 是”通行证”,证明你已经登录了
- 跨域时要注意安全,Token 不要泄露
10. 知识体系延伸 & 继续学习方向 & 遵守仓库规范文档
可学习的下一个知识点
- JWT 认证实现 — 学习如何使用 JWT 实现无状态的认证
- OAuth 2.0 授权流程 — 学习标准的第三方授权协议
- Session 管理 — 深入了解 Session 的工作原理和管理方式
- HTTP 基础 — 理解 Cookie、重定向等 HTTP 机制
- 加密算法 — 学习 Token 签名和加密的实现原理
- 微服务基础 — 了解微服务架构中的认证方案
继续学习方向
-
深入理解认证协议
- CAS 协议详细实现
- SAML 协议(企业级 SSO 标准)
- OpenID Connect(基于 OAuth 2.0 的身份层)
-
安全加固
- Token 加密和签名
- 防止 CSRF 攻击
- 防止 XSS 攻击
- 双因素认证(2FA)
-
性能优化
- Token 缓存策略
- 分布式 Session 管理
- Redis 实现 Session 共享
-
实际项目实践
- Spring Security + OAuth 2.0
- Node.js Passport.js
- 企业级 SSO 方案选型
遵守仓库规范文档
本文档遵循 语法规范 的格式要求,使用标准的 Markdown 语法和链接格式。