

中高端软件定制开发服务商

13245491521 13245491521
单点登录(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{ // 调用本地验证接口,检查是否有有效的会话 constresponse =awaitfetch('https://app1.example.com/api/auth/status', { credentials:'include'// 重要:包含跨域cookies if(response.ok) { constdata =awaitresponse.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,用于登录后重定向回来 constcurrentUrl =encodeURIComponent(window.location.href); // 重定向到SSO登录页面 window.location.href =`https://sso.example.com/login?redirect=${currentUrl}`; render() { const{ isAuthenticated, user, isLoading } =this.state; if(isLoading) { returndiv加载中.../div; } if(!isAuthenticated) { returndiv正在重定向到登录页面.../div; } return( div header p欢迎, {user.name}/p buttononClick={this.handleLogout}退出登录/button /header main{/* 应用内容 */}/main /div } handleLogout =async() = { try{ awaitfetch('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服务器 constresponse =awaitfetch('https://sso.example.com/api/login', { method:'POST', headers: { 'Content-Type':'application/json' }, body:JSON.stringify({ username, password }), credentials:'include' if(!response.ok) { consterror =awaitresponse.json(); thrownewError(error.message ||'登录失败'); } // 登录成功,获取重定向URL consturlParams =newURLSearchParams(window.location.search); constredirectUrl = 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服务器重定向回来) consturlParams =newURLSearchParams(window.location.search); consttoken = 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() = { consttoken = localStorage.getItem('auth_token'); if(!token) { this.setState({isLoading:false this.redirectToSSOLogin(); return; } try{ // 验证token有效性 constresponse =awaitfetch('https://app1.example.com/api/auth/validate', { headers: { 'Authorization':`Bearer${token}` } if(response.ok) { constuserData =awaitresponse.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 constappId ='app1'; constcallbackUrl =encodeURIComponent(window.location.origin); // 重定向到SSO登录 window.location.href =`https://sso.example.com/login?appId=${appId}&callback=${callbackUrl}`; handleLogout =()={ // 清除本地token localStorage.removeItem('auth_token'); // 重定向到SSO登出页面 constcallbackUrl =encodeURIComponent(window.location.origin); window.location.href =`https://sso.example.com/logout?callback=${callbackUrl}`; render() { const{ isAuthenticated, user, isLoading } =this.state; if(isLoading) { returndiv加载中.../div; } if(!isAuthenticated) { returndiv正在重定向到登录页面.../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参数 consturlParams =newURLSearchParams(window.location.search); constappId = urlParams.get('appId'); constcallbackUrl = urlParams.get('callback'); if(!appId || !callbackUrl) { thrownewError('缺少必要的参数'); } // 发送登录请求 constresponse =awaitfetch('https://sso.example.com/api/token', { method:'POST', headers: { 'Content-Type':'application/json' }, body:JSON.stringify({ username, password, appId }) if(!response.ok) { consterror =awaitresponse.json(); thrownewError(error.message ||'登录失败'); } // 获取JWT token const{ token } =awaitresponse.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实现SSO4.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 =()={ constaccessToken = localStorage.getItem('access_token'); consttokenExpiry = 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{ constresponse =awaitfetch('https://sso.example.com/api/userinfo', { headers: { 'Authorization':`Bearer${accessToken}` } if(response.ok) { constuserData =awaitresponse.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获取授权码 consturlParams =newURLSearchParams(window.location.search); constcode = urlParams.get('code'); conststate = urlParams.get('state'); // 验证state防止CSRF攻击 constsavedState = localStorage.getItem('oauth_state'); localStorage.removeItem('oauth_state'); if(!code || state !== savedState) { this.setState({ isLoading:false, error:'无效的OAuth回调' return; } try{ // 使用授权码获取访问令牌 consttokenResponse =awaitfetch(this.oauthConfig.tokenEndpoint, { method:'POST', headers: { 'Content-Type':'application/x-www-form-urlencoded' }, body:newURLSearchParams({ grant_type:'authorization_code', code, redirect_uri:this.oauthConfig.redirectUri, client_id:this.oauthConfig.clientId }) if(!tokenResponse.ok) { thrownewError('获取访问令牌失败'); } consttokenData =awaittokenResponse.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攻击 conststate =Math.random().toString(36).substring(2); localStorage.setItem('oauth_state', state); // 构建授权URL constauthUrl =newURL(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) { returndiv加载中.../div; } if(error) { returndivclassName="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问题的工具函数 constSSOUtils = { // 设置跨域请求选项 getCorsOptions() { return{ credentials:'include', headers: { 'Content-Type':'application/json' } }, // 使用iframe进行跨域通信 setupIframeMessaging() { // 创建隐藏的iframe,指向SSO域 constiframe =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) ={ // 创建唯一的回调函数名 constcallbackName ='jsonp_callback_'+Math.round(100000*Math.random()); // 创建script标签 constscript =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 constseparator = 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的特性 consttimestamp =newDate().getTime(); constimg =newImage(); // 创建Promise包装图片加载 constsessionCheck =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}`; constisAuthenticated =awaitsessionCheck; 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() { constaccessToken = localStorage.getItem('access_token'); consttokenExpiry = localStorage.getItem('token_expiry'); constrefreshToken = localStorage.getItem('refresh_token'); if(!accessToken || !tokenExpiry || !refreshToken) { return; } constexpiresAt =parseInt(tokenExpiry); constnow =newDate().getTime(); // 计算下次刷新时间 consttimeUntilRefresh = expiresAt - now -this.tokenExpiryThreshold; if(timeUntilRefresh =0) { // 如果token已经接近过期,立即刷新 this.refreshToken(); }else{ // 设置定时器,在token接近过期时刷新 setTimeout(()=this.refreshToken(), timeUntilRefresh); } } // 刷新token refreshToken() { // 如果已经有一个刷新请求在进行中,返回该Promise if(this.refreshPromise) { returnthis.refreshPromise; } constrefreshToken = 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:newURLSearchParams({ grant_type:'refresh_token', refresh_token: refreshToken, client_id:this.clientId }) }) .then(response={ if(!response.ok) { thrownewError('刷新令牌失败'); } returnresponse.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); } // 设置下一次刷新的定时器 constnextRefreshTime = data.expires_in *1000-this.tokenExpiryThreshold; setTimeout(()=this.refreshToken(), nextRefreshTime); // 返回新的token数据 returndata; }) .catch(error={ // 刷新失败,可能需要重新登录 console.error('刷新令牌失败:', error); // 清除无效的token localStorage.removeItem('access_token'); localStorage.removeItem('token_expiry'); localStorage.removeItem('refresh_token'); // 触发登录流程 window.dispatchEvent(newCustomEvent('auth:required')); throwerror; }) .finally(()={ // 清除进行中的Promise引用 this.refreshPromise =null; returnthis.refreshPromise; } // 获取有效的访问令牌 getAccessToken() { constaccessToken = localStorage.getItem('access_token'); consttokenExpiry = localStorage.getItem('token_expiry'); if(!accessToken || !tokenExpiry) { // 没有token,需要登录 window.dispatchEvent(newCustomEvent('auth:required')); returnPromise.reject(newError('未登录')); } constexpiresAt =parseInt(tokenExpiry); constnow =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 =newTokenRefreshManager({ 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{ // 获取有效的访问令牌 consttoken =awaitthis.tokenManager.getAccessToken(); // 将token添加到请求头 config.headers.Authorization =`Bearer${token}`; returnconfig; }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重试请求 consttoken = localStorage.getItem('access_token'); error.config.headers.Authorization =`Bearer${token}`; returnthis.axiosInstance.request(error.config); }catch(refreshError) { // 刷新失败,需要重新登录 window.dispatchEvent(newCustomEvent('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流程总结「用户访问应用」: 前端应用检查本地存储中是否有有效的认证信息如果没有,重定向到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 =newSecureStorage(); // CSRF保护 this.csrfToken =this.generateRandomToken(); } // 生成随机令牌 generateRandomToken(length =32) { constarray =newUint8Array(length); window.crypto.getRandomValues(array); returnArray.from(array, byte = byte.toString(16).padStart(2,'0')).join(''); } // 安全存储实现 classSecureStorage{ // 使用加密存储敏感信息 setItem(key, value) { // 对于敏感数据,可以考虑使用Web Crypto API进行加密 constencryptedValue =this.encrypt(value); sessionStorage.setItem(key, encryptedValue); } getItem(key) { constencryptedValue = sessionStorage.getItem(key); if(!encryptedValue)returnnull; returnthis.decrypt(encryptedValue); } removeItem(key) { sessionStorage.removeItem(key); } // 简单加密实现(实际应用中应使用更强的加密) encrypt(value) { // 这里应该使用Web Crypto API进行真正的加密 // 这只是一个示例 returnbtoa(value); } decrypt(encryptedValue) { // 对应的解密 returnatob(encryptedValue); } } // 防止点击劫持 preventClickjacking() { // 设置X-Frame-Options头(服务器端) // 前端可以检测是否在iframe中 if(window.self !==window.top) { // 可能在iframe中,根据策略决定是否继续 document.body.innerHTML ='为了安全,此页面不允许在iframe中显示'; } } // XSS防护 sanitizeInput(input) { // 使用DOMPurify库清理用户输入 returnDOMPurify.sanitize(input); } } 总结单点登录(SSO)是现代Web应用中常用的身份验证机制,它允许用户使用一组凭据访问多个应用程序。从前端角度实现SSO主要有三种方式: 「基于Cookie的SSO」:依赖共享域的Cookie,简单但有跨域限制「基于Token的SSO」:使用JWT等令牌,更灵活,适合分布式系统「OAuth 2.0/OpenID Connect」:标准化的授权框架,支持第三方应用授权无论采用哪种方式,前端实现都需要处理: 认证状态检查登录流程会话维护令牌刷新安全防护单点登出通过合理的架构设计和安全实践,可以构建出用户体验良好、安全可靠的单点登录系统。 AI编程资讯AI Coding专区指南: https://aicoding.juejin.cn/aicoding 点击"阅读原文"了解详情~ 阅读原文
| 上一篇:2025-08-10_联合利华调整预算后 , 全球网红越来越贵 | 下一篇:2018-06-19_资源 | 实时评估世界杯球员的正确姿势:FAIR开源DensePose |
TAG标签: |
15 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
