稀土掘金技术社区

单点登录(SSO)全流程详解

图片

(金石瓜分计划上线,速戳上图了解详情)

单点登录(Single Sign-On,SSO)是一种身份验证机制,允许用户使用一组凭据访问多个应用程序。下面从前端角度详细讲解SSO的完整流程。

1. SSO架构概述

SSO系统通常包含三个主要组件:

  • 「SSO服务器」:中央认证服务,负责用户身份验证
  • 「客户端应用」:需要用户登录的各个应用
  • 「用户浏览器」:用户交互界面

2. 基于Cookie的SSO实现

2.1 登录流程代码实现

// 前端应用入口组件
classAppextendsReact.Component{
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true
    };
  }

  componentDidMount() {
// 检查用户是否已登录
this.checkLoginStatus();
  }

  checkLoginStatus = async () => {
try {
// 调用本地验证接口,检查是否有有效的会话
const response = await fetch('https://app1.example.com/api/auth/status', {
credentials: 'include'// 重要:包含跨域cookies
      });

if (response.ok) {
const data = await response.json();
if (data.isAuthenticated) {
this.setState({ 
isAuthenticated: true, 
user: data.user,
isLoading: false
          });
return;
        }
      }

// 如果未登录,重定向到SSO登录页
this.redirectToSSOLogin();
    } catch (error) {
console.error('验证登录状态失败:', error);
this.setState({ isLoading: false });
    }
  };

  redirectToSSOLogin = () => {
// 当前应用URL,用于登录后重定向回来
const currentUrl = encodeURIComponent(window.location.href);

// 重定向到SSO登录页面
window.location.href = `https://sso.example.com/login?redirect=${currentUrl}`;
  };

  render() {
const { isAuthenticated, user, isLoading } = this.state;

if (isLoading) {
return<div>加载中...</div>;
    }

if (!isAuthenticated) {
return<div>正在重定向到登录页面...</div>;
    }

return (
<div>
<header>
<p>欢迎, {user.name}</p>
<buttononClick={this.handleLogout}>退出登录</button>
</header>
<main>{/* 应用内容 */}</main>
</div>

    );
  }

  handleLogout = async () => {
try {
await fetch('https://sso.example.com/logout', {
method: 'POST',
credentials: 'include'
      });

// 登出后重定向到登录页
window.location.href = 'https://sso.example.com/login';
    } catch (error) {
console.error('登出失败:', error);
    }
  };
}

2.2 SSO登录页面实现

// SSO服务器上的登录页面组件
classSSOLoginPageextendsReact.Component{
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
error: null,
isLoading: false
    };
  }

  handleInputChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
  };

  handleSubmit = async (e) => {
    e.preventDefault();
this.setState({ isLoading: true, error: null });

try {
const { username, password } = this.state;

// 发送登录请求到SSO服务器
const response = await fetch('https://sso.example.com/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
        },
body: JSON.stringify({ username, password }),
credentials: 'include'
      });

if (!response.ok) {
const error = await response.json();
thrownewError(error.message || '登录失败');
      }

// 登录成功,获取重定向URL
const urlParams = new URLSearchParams(window.location.search);
const redirectUrl = urlParams.get('redirect') || 'https://app1.example.com';

// 重定向回原应用
window.location.href = redirectUrl;
    } catch (error) {
this.setState({ 
error: error.message, 
isLoading: false
      });
    }
  };

  render() {
const { username, password, error, isLoading } = this.state;

return (
<divclassName="login-container">
<h2>统一登录平台</h2>
        {error && <divclassName="error-message">{error}</div>}
<formonSubmit={this.handleSubmit}>
<divclassName="form-group">
<labelhtmlFor="username">用户名</label>
<input
type="text"
id="username"
name="username"
value={username}
onChange={this.handleInputChange}
required
            />

</div>
<divclassName="form-group">
<labelhtmlFor="password">密码</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={this.handleInputChange}
required
            />

</div>
<buttontype="submit"disabled={isLoading}>
            {isLoading ? '登录中...' : '登录'}
</button>
</form>
</div>

    );
  }
}

3. 基于Token的SSO实现(JWT)

3.1 前端应用入口

// 使用JWT实现的SSO前端应用
classTokenBasedAppextendsReact.Component{
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true
    };
  }

  componentDidMount() {
// 检查URL中是否有token参数(从SSO服务器重定向回来)
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');

if (token) {
// 保存token到localStorage
      localStorage.setItem('auth_token', token);

// 清除URL中的token参数
window.history.replaceState({}, document.title, window.location.pathname);
    }

// 验证token
this.validateToken();
  }

  validateToken = async () => {
const token = localStorage.getItem('auth_token');

if (!token) {
this.setState({ isLoading: false });
this.redirectToSSOLogin();
return;
    }

try {
// 验证token有效性
const response = await fetch('https://app1.example.com/api/auth/validate', {
headers: {
'Authorization': `Bearer ${token}`
        }
      });

if (response.ok) {
const userData = await response.json();
this.setState({
isAuthenticated: true,
user: userData,
isLoading: false
        });
      } else {
// token无效,清除并重定向到登录
        localStorage.removeItem('auth_token');
this.setState({ isLoading: false });
this.redirectToSSOLogin();
      }
    } catch (error) {
console.error('Token验证失败:', error);
this.setState({ isLoading: false });
this.redirectToSSOLogin();
    }
  };

  redirectToSSOLogin = () => {
// 应用ID和回调URL
const appId = 'app1';
const callbackUrl = encodeURIComponent(window.location.origin);

// 重定向到SSO登录
window.location.href = `https://sso.example.com/login?appId=${appId}&callback=${callbackUrl}`;
  };

  handleLogout = () => {
// 清除本地token
    localStorage.removeItem('auth_token');

// 重定向到SSO登出页面
const callbackUrl = encodeURIComponent(window.location.origin);
window.location.href = `https://sso.example.com/logout?callback=${callbackUrl}`;
  };

  render() {
const { isAuthenticated, user, isLoading } = this.state;

if (isLoading) {
return<div>加载中...</div>;
    }

if (!isAuthenticated) {
return<div>正在重定向到登录页面...</div>;
    }

return (
<div>
<header>
<p>欢迎, {user.name}</p>
<buttononClick={this.handleLogout}>退出登录</button>
</header>
<main>{/* 应用内容 */}</main>
</div>

    );
  }
}

3.2 JWT登录页面

// SSO服务器上的JWT登录页面
classJWTLoginPageextendsReact.Component{
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
error: null,
isLoading: false
    };
  }

  handleInputChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
  };

  handleSubmit = async (e) => {
    e.preventDefault();
this.setState({ isLoading: true, error: null });

try {
const { username, password } = this.state;

// 获取URL参数
const urlParams = new URLSearchParams(window.location.search);
const appId = urlParams.get('appId');
const callbackUrl = urlParams.get('callback');

if (!appId || !callbackUrl) {
thrownewError('缺少必要的参数');
      }

// 发送登录请求
const response = await fetch('https://sso.example.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
        },
body: JSON.stringify({ 
          username, 
          password,
          appId
        })
      });

if (!response.ok) {
const error = await response.json();
thrownewError(error.message || '登录失败');
      }

// 获取JWT token
const { token } = await response.json();

// 重定向回应用,并带上token
window.location.href = `${decodeURIComponent(callbackUrl)}?token=${token}`;
    } catch (error) {
this.setState({ 
error: error.message, 
isLoading: false
      });
    }
  };

  render() {
const { username, password, error, isLoading } = this.state;

return (
<divclassName="login-container">
<h2>统一登录平台</h2>
        {error && <divclassName="error-message">{error}</div>}
<formonSubmit={this.handleSubmit}>
<divclassName="form-group">
<labelhtmlFor="username">用户名</label>
<input
type="text"
id="username"
name="username"
value={username}
onChange={this.handleInputChange}
required
            />

</div>
<divclassName="form-group">
<labelhtmlFor="password">密码</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={this.handleInputChange}
required
            />

</div>
<buttontype="submit"disabled={isLoading}>
            {isLoading ? '登录中...' : '登录'}
</button>
</form>
</div>

    );
  }
}

4. 使用OAuth 2.0实现SSO

4.1 前端应用OAuth流程

// 使用OAuth 2.0实现的SSO前端应用
classOAuthAppextendsReact.Component{
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true
    };

// OAuth配置
this.oauthConfig = {
clientId: 'your-client-id',
redirectUri: `${window.location.origin}/callback`,
authorizationEndpoint: 'https://sso.example.com/oauth/authorize',
tokenEndpoint: 'https://sso.example.com/oauth/token',
scope: 'profile email'
    };
  }

  componentDidMount() {
// 检查是否在OAuth回调页面
if (window.location.pathname === '/callback') {
this.handleOAuthCallback();
    } else {
this.checkAuthentication();
    }
  }

  checkAuthentication = () => {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');

// 检查token是否存在且未过期
if (accessToken && tokenExpiry && newDate().getTime() < parseInt(tokenExpiry)) {
this.fetchUserInfo(accessToken);
    } else {
// 清除过期token
if (accessToken) {
        localStorage.removeItem('access_token');
        localStorage.removeItem('token_expiry');
        localStorage.removeItem('refresh_token');
      }

this.setState({ isLoading: false });
    }
  };

  fetchUserInfo = async (accessToken) => {
try {
const response = await fetch('https://sso.example.com/api/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
        }
      });

if (response.ok) {
const userData = await response.json();
this.setState({
isAuthenticated: true,
user: userData,
isLoading: false
        });
      } else {
// token可能无效
this.setState({ isLoading: false });
this.initiateOAuthFlow();
      }
    } catch (error) {
console.error('获取用户信息失败:', error);
this.setState({ isLoading: false });
    }
  };

  handleOAuthCallback = async () => {
// 从URL获取授权码
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');

// 验证state防止CSRF攻击
const savedState = localStorage.getItem('oauth_state');
    localStorage.removeItem('oauth_state');

if (!code || state !== savedState) {
this.setState({ 
isLoading: false,
error: '无效的OAuth回调'
      });
return;
    }

try {
// 使用授权码获取访问令牌
const tokenResponse = await fetch(this.oauthConfig.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
        },
body: new URLSearchParams({
grant_type: 'authorization_code',
          code,
redirect_uri: this.oauthConfig.redirectUri,
client_id: this.oauthConfig.clientId
        })
      });

if (!tokenResponse.ok) {
thrownewError('获取访问令牌失败');
      }

const tokenData = await tokenResponse.json();

// 保存token
      localStorage.setItem('access_token', tokenData.access_token);
      localStorage.setItem('token_expiry', (newDate().getTime() + tokenData.expires_in * 1000).toString());

if (tokenData.refresh_token) {
        localStorage.setItem('refresh_token', tokenData.refresh_token);
      }

// 获取用户信息
awaitthis.fetchUserInfo(tokenData.access_token);

// 重定向到应用首页
window.history.replaceState({}, document.title, '/');
    } catch (error) {
console.error('处理OAuth回调失败:', error);
this.setState({ 
isLoading: false,
error: error.message
      });
    }
  };

  initiateOAuthFlow = () => {
// 生成随机state参数防止CSRF攻击
const state = Math.random().toString(36).substring(2);
    localStorage.setItem('oauth_state', state);

// 构建授权URL
const authUrl = new URL(this.oauthConfig.authorizationEndpoint);
    authUrl.searchParams.append('client_id', this.oauthConfig.clientId);
    authUrl.searchParams.append('redirect_uri', this.oauthConfig.redirectUri);
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', this.oauthConfig.scope);
    authUrl.searchParams.append('state', state);

// 重定向到授权页面
window.location.href = authUrl.toString();
  };

  handleLogout = async () => {
// 清除本地存储的token
    localStorage.removeItem('access_token');
    localStorage.removeItem('token_expiry');
    localStorage.removeItem('refresh_token');

// 重定向到SSO登出页面
window.location.href = `https://sso.example.com/logout?redirect_uri=${encodeURIComponent(window.location.origin)}`;
  };

  render() {
const { isAuthenticated, user, isLoading, error } = this.state;

if (isLoading) {
return<div>加载中...</div>;
    }

if (error) {
return<divclassName="error-message">{error}</div>;
    }

if (!isAuthenticated) {
return (
<div>
<h2>请登录以继续</h2>
<buttononClick={this.initiateOAuthFlow}>使用SSO登录</button>
</div>

      );
    }

return (
<div>
<header>
<p>欢迎, {user.name}</p>
<buttononClick={this.handleLogout}>退出登录</button>
</header>
<main>{/* 应用内容 */}</main>
</div>

    );
  }
}

5. 跨域问题解决方案

// 处理跨域Cookie问题的工具函数
const SSOUtils = {
// 设置跨域请求选项
  getCorsOptions() {
return {
credentials: 'include',
headers: {
'Content-Type': 'application/json'
      }
    };
  },

// 使用iframe进行跨域通信
  setupIframeMessaging() {
// 创建隐藏的iframe,指向SSO域
const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'https://sso.example.com/session-bridge.html';
document.body.appendChild(iframe);

returnnewPromise((resolve) => {
// 监听来自iframe的消息
window.addEventListener('message', functionmessageHandler(event) {
// 验证消息来源
if (event.origin !== 'https://sso.example.com') return;

// 处理会话信息
if (event.data.type === 'SESSION_INFO') {
window.removeEventListener('message', messageHandler);
          resolve(event.data.payload);
        }
      });
    });
  },

// 使用JSONP解决跨域问题
  fetchWithJsonp(url, callbackParam = 'callback') {
returnnewPromise((resolve, reject) => {
// 创建唯一的回调函数名
const callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());

// 创建script标签
const script = document.createElement('script');

// 设置全局回调函数
window[callbackName] = (data) => {
// 清理:删除script标签和全局回调
deletewindow[callbackName];
document.body.removeChild(script);
        resolve(data);
      };

// 处理错误情况
      script.onerror = () => {
deletewindow[callbackName];
document.body.removeChild(script);
        reject(newError('JSONP请求失败'));
      };

// 构建带有回调参数的URL
const separator = url.indexOf('?') !== -1 ? '&' : '?';
      script.src = `${url}${separator}${callbackParam}=${callbackName}`;

// 添加到文档中执行请求
document.body.appendChild(script);
    });
  }
};

6. 前端SSO会话检查组件

// 会话检查组件,可以集成到任何应用中
classSSOSessionCheckerextendsReact.Component{
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
isChecking: true
    };

// 检查间隔时间(毫秒)
this.checkInterval = props.checkInterval || 5 * 60 * 1000; // 默认5分钟
this.intervalId = null;
  }

  componentDidMount() {
// 初始检查
this.checkSession();

// 设置定期检查
this.intervalId = setInterval(this.checkSession, this.checkInterval);

// 监听浏览器标签页激活事件,重新检查会话
document.addEventListener('visibilitychange', this.handleVisibilityChange);
  }

  componentWillUnmount() {
// 清理定时器和事件监听
if (this.intervalId) {
      clearInterval(this.intervalId);
    }
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
  }

  handleVisibilityChange = () => {
// 当用户切换回标签页时检查会话
if (document.visibilityState === 'visible') {
this.checkSession();
    }
  };

  checkSession = async () => {
this.setState({ isChecking: true });

try {
// 使用图片探测技术检查SSO会话状态
// 这种方法利用了图片加载会携带cookies的特性
const timestamp = newDate().getTime();
const img = new Image();

// 创建Promise包装图片加载
const sessionCheck = newPromise((resolve, reject) => {
        img.onload = () => resolve(true); // 图片加载成功,表示会话有效
        img.onerror = () => resolve(false); // 图片加载失败,表示会话无效

// 5秒超时
        setTimeout(() => reject(newError('会话检查超时')), 5000);
      });

// 设置图片源,触发请求
      img.src = `https://sso.example.com/session-check.png?t=${timestamp}`;

const isAuthenticated = await sessionCheck;

this.setState({ 
        isAuthenticated,
isChecking: false
      });

// 如果会话已失效,通知父组件
if (!isAuthenticated && this.props.onSessionExpired) {
this.props.onSessionExpired();
      }
    } catch (error) {
console.error('会话检查失败:', error);
this.setState({ isChecking: false });
    }
  };

  render() {
// 将会话状态传递给子组件
returnthis.props.children({
isAuthenticated: this.state.isAuthenticated,
isChecking: this.state.isChecking,
checkSession: this.checkSession
    });
  }
}

7. 实现无感刷新Token

// Token自动刷新管理器
classTokenRefreshManager{
constructor(options) {
this.refreshEndpoint = options.refreshEndpoint || 'https://sso.example.com/oauth/token';
this.clientId = options.clientId;
this.tokenExpiryThreshold = options.tokenExpiryThreshold || 5 * 60 * 1000; // 默认提前5分钟刷新
this.refreshPromise = null;
  }

// 初始化刷新计时器
  setupRefreshTimer() {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');
const refreshToken = localStorage.getItem('refresh_token');

if (!accessToken || !tokenExpiry || !refreshToken) {
return;
    }

const expiresAt = parseInt(tokenExpiry);
const now = newDate().getTime();

// 计算下次刷新时间
const timeUntilRefresh = expiresAt - now - this.tokenExpiryThreshold;

if (timeUntilRefresh <= 0) {
// 如果token已经接近过期,立即刷新
this.refreshToken();
    } else {
// 设置定时器,在token接近过期时刷新
      setTimeout(() =>this.refreshToken(), timeUntilRefresh);
    }
  }

// 刷新token
  refreshToken() {
// 如果已经有一个刷新请求在进行中,返回该Promise
if (this.refreshPromise) {
returnthis.refreshPromise;
    }

const refreshToken = localStorage.getItem('refresh_token');

if (!refreshToken) {
returnPromise.reject(newError('没有可用的刷新令牌'));
    }

// 创建刷新token的请求
this.refreshPromise = fetch(this.refreshEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
      },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId
      })
    })
    .then(response => {
if (!response.ok) {
thrownewError('刷新令牌失败');
      }
return response.json();
    })
    .then(data => {
// 更新存储的token
      localStorage.setItem('access_token', data.access_token);
      localStorage.setItem('token_expiry', (newDate().getTime() + data.expires_in * 1000).toString());

if (data.refresh_token) {
        localStorage.setItem('refresh_token', data.refresh_token);
      }

// 设置下一次刷新的定时器
const nextRefreshTime = data.expires_in * 1000 - this.tokenExpiryThreshold;
      setTimeout(() =>this.refreshToken(), nextRefreshTime);

// 返回新的token数据
return data;
    })
    .catch(error => {
// 刷新失败,可能需要重新登录
console.error('刷新令牌失败:', error);

// 清除无效的token
      localStorage.removeItem('access_token');
      localStorage.removeItem('token_expiry');
      localStorage.removeItem('refresh_token');

// 触发登录流程
window.dispatchEvent(new CustomEvent('auth:required'));

throw error;
    })
    .finally(() => {
// 清除进行中的Promise引用
this.refreshPromise = null;
    });

returnthis.refreshPromise;
  }

// 获取有效的访问令牌
  getAccessToken() {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');

if (!accessToken || !tokenExpiry) {
// 没有token,需要登录
window.dispatchEvent(new CustomEvent('auth:required'));
returnPromise.reject(newError('未登录'));
    }

const expiresAt = parseInt(tokenExpiry);
const now = newDate().getTime();

// 如果token即将过期,刷新它
if (expiresAt - now < this.tokenExpiryThreshold) {
returnthis.refreshToken().then(data => data.access_token);
    }

// 返回现有的有效token
returnPromise.resolve(accessToken);
  }
}

8. 前端API请求拦截器

// 使用Axios拦截器自动添加认证token
classApiClient{
constructor() {
this.tokenManager = new TokenRefreshManager({
clientId: 'your-client-id',
refreshEndpoint: 'https://sso.example.com/oauth/token'
    });

// 初始化Axios实例
this.axiosInstance = axios.create({
baseURL: 'https://api.example.com'
    });

// 设置请求拦截器
this.axiosInstance.interceptors.request.use(
async (config) => {
try {
// 获取有效的访问令牌
const token = awaitthis.tokenManager.getAccessToken();

// 将token添加到请求头
          config.headers.Authorization = `Bearer ${token}`;
return config;
        } catch (error) {
// 获取token失败
returnPromise.reject(error);
        }
      },
      (error) => Promise.reject(error)
    );

// 设置响应拦截器
this.axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
// 检查是否是401错误(未授权)
if (error.response && error.response.status === 401) {
try {
// 尝试刷新token
awaitthis.tokenManager.refreshToken();

// 使用新token重试请求
const token = localStorage.getItem('access_token');
            error.config.headers.Authorization = `Bearer ${token}`;
returnthis.axiosInstance.request(error.config);
          } catch (refreshError) {
// 刷新失败,需要重新登录
window.dispatchEvent(new CustomEvent('auth:required'));
returnPromise.reject(refreshError);
          }
        }

returnPromise.reject(error);
      }
    );

// 初始化token刷新计时器
this.tokenManager.setupRefreshTimer();
  }

// 封装API请求方法
get(url, config) {
returnthis.axiosInstance.get(url, config);
  }

  post(url, data, config) {
returnthis.axiosInstance.post(url, data, config);
  }

  put(url, data, config) {
returnthis.axiosInstance.put(url, data, config);
  }

delete(url, config) {
returnthis.axiosInstance.delete(url, config);
  }
}

9. 完整的SSO流程总结

  1. 「用户访问应用」:

  • 前端应用检查本地存储中是否有有效的认证信息
  • 如果没有,重定向到SSO登录页面
  • 「SSO登录」:

    • 用户在SSO服务器上输入凭据
    • SSO服务器验证凭据并创建会话
    • 根据SSO实现方式,生成Cookie或Token
  • 「重定向回应用」:

    • 对于Cookie-based SSO:重定向回应用,带上会话Cookie
    • 对于Token-based SSO:重定向回应用,带上token参数
    • 对于OAuth流程:先获取授权码,然后用授权码换取访问令牌
  • 「应用验证认证」:

    • 验证Cookie或Token的有效性
    • 获取用户信息
    • 建立应用内的用户会话
  • 「会话维护」:

    • 定期检查SSO会话状态
    • 自动刷新即将过期的Token
    • 处理会话过期的情况
  • 「单点登出」:

    • 用户点击登出按钮
    • 应用清除本地认证信息
    • 重定向到SSO登出端点,清除SSO会话
    • SSO服务器通知所有应用登出(可选)

    10. 安全最佳实践

    // 安全增强的SSO客户端
    classSecureSSO{
    constructor() {
    // 使用安全的存储方式
    this.storage = new SecureStorage();

    // CSRF保护
    this.csrfToken = this.generateRandomToken();
      }

    // 生成随机令牌
      generateRandomToken(length = 32) {
    const array = newUint8Array(length);
    window.crypto.getRandomValues(array);
    returnArray.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
      }

    // 安全存储实现
    classSecureStorage{
    // 使用加密存储敏感信息
        setItem(key, value) {
    // 对于敏感数据,可以考虑使用Web Crypto API进行加密
    const encryptedValue = this.encrypt(value);
          sessionStorage.setItem(key, encryptedValue);
        }

        getItem(key) {
    const encryptedValue = sessionStorage.getItem(key);
    if (!encryptedValue) returnnull;
    returnthis.decrypt(encryptedValue);
        }

        removeItem(key) {
          sessionStorage.removeItem(key);
        }

    // 简单加密实现(实际应用中应使用更强的加密)
        encrypt(value) {
    // 这里应该使用Web Crypto API进行真正的加密
    // 这只是一个示例
    return btoa(value);
        }

        decrypt(encryptedValue) {
    // 对应的解密
    return atob(encryptedValue);
        }
      }

    // 防止点击劫持
      preventClickjacking() {
    // 设置X-Frame-Options头(服务器端)
    // 前端可以检测是否在iframe中
    if (window.self !== window.top) {
    // 可能在iframe中,根据策略决定是否继续
    document.body.innerHTML = '为了安全,此页面不允许在iframe中显示';
        }
      }

    // XSS防护
      sanitizeInput(input) {
    // 使用DOMPurify库清理用户输入
    return DOMPurify.sanitize(input);
      }
    }

    总结

    单点登录(SSO)是现代Web应用中常用的身份验证机制,它允许用户使用一组凭据访问多个应用程序。从前端角度实现SSO主要有三种方式:

    1. 「基于Cookie的SSO」:依赖共享域的Cookie,简单但有跨域限制
    2. 「基于Token的SSO」:使用JWT等令牌,更灵活,适合分布式系统
    3. 「OAuth 2.0/OpenID Connect」:标准化的授权框架,支持第三方应用授权

    无论采用哪种方式,前端实现都需要处理:

    • 认证状态检查
    • 登录流程
    • 会话维护
    • 令牌刷新
    • 安全防护
    • 单点登出

    通过合理的架构设计和安全实践,可以构建出用户体验良好、安全可靠的单点登录系统。

    AI编程资讯AI Coding专区指南:

    https://aicoding.juejin.cn/aicoding

    图片
    点击"阅读原文"了解详情~