单点登录(SSO)详解

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

1. SSO架构概述

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

  • 「SSO服务器」:中央认证服务,负责用户身份验证

  • 「客户端应用」:需要用户登录的各个应用

  • 「用户浏览器」:用户交互界面

2. 基于Cookie的SSO实现

2.1 登录流程代码实现

// 前端应用入口组件
class App extends React.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>
          <button onClick={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服务器上的登录页面组件
class SSOLoginPage extends React.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 (
      <div className="login-container">
        <h2>统一登录平台</h2>
        {error && <div className="error-message">{error}</div>}
        <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <label htmlFor="username">用户名</label>
            <input
              type="text"
              id="username"
              name="username"
              value={username}
              onChange={this.handleInputChange}
              required
            />
          </div>
          <div className="form-group">
            <label htmlFor="password">密码</label>
            <input
              type="password"
              id="password"
              name="password"
              value={password}
              onChange={this.handleInputChange}
              required
            />
          </div>
          <button type="submit" disabled={isLoading}>
            {isLoading ? '登录中...' : '登录'}
          </button>
        </form>
      </div>
    );
  }
}

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

3.1 前端应用入口

// 使用JWT实现的SSO前端应用
class TokenBasedApp extends React.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>
          <button onClick={this.handleLogout}>退出登录</button>
        </header>
        <main>{/* 应用内容 */}</main>
      </div>
    );
  }
}

3.2 JWT登录页面

// SSO服务器上的JWT登录页面
class JWTLoginPage extends React.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 (
      <div className="login-container">
        <h2>统一登录平台</h2>
        {error && <div className="error-message">{error}</div>}
        <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <label htmlFor="username">用户名</label>
            <input
              type="text"
              id="username"
              name="username"
              value={username}
              onChange={this.handleInputChange}
              required
            />
          </div>
          <div className="form-group">
            <label htmlFor="password">密码</label>
            <input
              type="password"
              id="password"
              name="password"
              value={password}
              onChange={this.handleInputChange}
              required
            />
          </div>
          <button type="submit" disabled={isLoading}>
            {isLoading ? '登录中...' : '登录'}
          </button>
        </form>
      </div>
    );
  }
}

4. 使用OAuth 2.0实现SSO

4.1 前端应用OAuth流程

// 使用OAuth 2.0实现的SSO前端应用
class OAuthApp extends React.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<div className="error-message">{error}</div>;
    }

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

    return (
      <div>
        <header>
          <p>欢迎, {user.name}</p>
          <button onClick={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', function messageHandler(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会话检查组件

// 会话检查组件,可以集成到任何应用中
class SSOSessionChecker extends React.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自动刷新管理器
class TokenRefreshManager {
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
class ApiClient {
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登录页面

  2. 「SSO登录」

    • 用户在SSO服务器上输入凭据

    • SSO服务器验证凭据并创建会话

    • 根据SSO实现方式,生成Cookie或Token

  3. 「重定向回应用」

    • 对于Cookie-based SSO:重定向回应用,带上会话Cookie

    • 对于Token-based SSO:重定向回应用,带上token参数

    • 对于OAuth流程:先获取授权码,然后用授权码换取访问令牌

  4. 「应用验证认证」

    • 验证Cookie或Token的有效性

    • 获取用户信息

    • 建立应用内的用户会话

  5. 「会话维护」

    • 定期检查SSO会话状态

    • 自动刷新即将过期的Token

    • 处理会话过期的情况

  6. 「单点登出」

    • 用户点击登出按钮

    • 应用清除本地认证信息

    • 重定向到SSO登出端点,清除SSO会话

    • SSO服务器通知所有应用登出(可选)

10. 安全最佳实践

// 安全增强的SSO客户端
class SecureSSO {
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('');
  }

// 安全存储实现
class SecureStorage {
    // 使用加密存储敏感信息
    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」:标准化的授权框架,支持第三方应用授权

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

  • 认证状态检查

  • 登录流程

  • 会话维护

  • 令牌刷新

  • 安全防护

  • 单点登出

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值