在线客服

前端单点登录(SSO)方案思考

adminadmin 报建百科 2024-04-25 158 13
前端单点登录(SSO)方案思考

什么是单点登录

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

SSO一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport发起认证。

举个例子,比如淘宝、天猫都属于阿里旗下的产品,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象背后就是用单点登录实现的。再比如百度贴吧和百度地图是百度公司旗下的两个不同的应用系统,如果用户在百度贴吧登录过之后,当他访问百度地图时无需再次登录,那么就说明百度贴吧和百度地图之间实现了单点登录。

SSO 机制实现流程

用户首次访问时,需要在认证中心登录:

  1. 用户访问网站%20a.com%20下的%20pageA%20页面。
  2. 由于没有登录,则会重定向到认证中心,并带上回调地址%20www.sso.com?return_uri=a.com/pageA,以便登录后直接进入对应页面。
  3. 用户在认证中心输入账号密码,提交登录申请。
  4. 认证中心验证账号密码是否有效,通过后创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌,sso认证中心带着令牌跳转会最初的请求地址,即重定向回%20a.com?ticket=123,并带上了令牌(授权码)%20ticket,并将认证中心%20sso.com%20的登录态写入%20Cookie
  5. 在%20a.com%20服务器中,拿着%20令牌ticket%20向认证中心确认令牌%20ticket是否真实有效。
  6. 验证成功后,服务器将登录信息写入%20Cookie(此时客户端有%202%20个%20Cookie%20分别存有%20a.com%20和%20sso.com%20的登录态)。
  7. a.com使用该令牌创建与用户的局部会话,返回受保护资源
全局会话和局部会话

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁
在认证中心登录完成之后,继续访问%20a.com%20下的其他页面:

%20这个时候,由于%20a.com%20存在已登录的%20Cookie%20信息,所以服务器端直接认证成功。

如果认证中心登录完成之后,访问%20b.com%20下的页面:

%20这个时候,由于认证中心存在之前登录过的%20Cookie,所以也不用再次输入账号密码,直接返回第%204%20步,下发%20ticket%20给%20b.com%20即可。大概流程就是:

  1. 所谓的同平台下的另一个子系统的用户访问b.com的受保护资源

  2. b.com发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

  3. sso认证中心发现用户已登录(a.com已经登录过了),跳转回b.com的地址,并附上令牌

  4. b.com拿到令牌,去sso认证中心校验令牌是否有效

  5. sso认证中心校验令牌,返回有效,注册b.com

  6. b.com使用该令牌创建与用户的局部会话,返回受保护资源

SSO%20机制实现方式

单点登录主要有三种实现方式(同域SSO不用设置独立的%20SSO%20服务器,因为业务后台服务器本身就足以承担%20SSO%20的职能。):

  1. 父域%20Cookie,和同域SSO不同在于,服务器在返回%20cookie%20的时候,要把cookie%20的%20domain%20设置为其父域。
  2. 认证中心
  3. LocalStorage%20跨域

一般情况下,用户的登录状态是记录在%20Session%20中的,要实现共享登录状态,就要先共享%20Session,但是由于不同的应用系统有着不同的域名,尽管%20Session%20共享了,但是由于%20SessionId%20是往往保存在浏览器%20Cookie%20中的,因此存在作用域的限制,无法跨域名传递,也就是说当用户在%20a.com%20中登录后,Session%20Id%20仅在浏览器访问%20a.com%20时才会自动在请求头中携带,而当浏览器访问%20b.com%20时,Session%20Id%20是不会被带过去的。实现单点登录的关键在于,如何让%20Session%20Id(或%20Token)在多个域中共享。

1.%20父域%20Cookie

Cookie%20的作用域由%20domain%20属性和%20path%20属性共同决定。domain%20属性的有效值为当前域或其父域的域名/IP地址,在%20Tomcat%20中,domain%20属性默认为当前域的域名/IP地址。path%20属性的有效值是以“/”开头的路径,在%20Tomcat%20中,path%20属性默认为当前%20Web%20应用的上下文路径。

如果将%20Cookie%20的%20domain%20属性设置为当前域的父域,那么就认为它是父域%20CookieCookie%20有一个特点,即父域中的%20Cookie%20被子域所共享,也就是说,子域会自动继承父域中的%20Cookie

利用%20Cookie%20的这个特点,可以将%20Session%20Id(或%20Token)保存到父域中就可以了。我们只需要将%20Cookie%20的%20domain%20属性设置为父域的域名(主域名),同时将%20Cookie%20的%20path%20属性设置为根路径,这样所有的子域应用就都可以访问到这个%20Cookie%20了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如%20tieba.baidu.com%20和%20map.baidu.com,它们都建立在%20baidu.com%20这个主域名之下,那么它们就可以通过这种方式来实现单点登录。

总结:虽然我们可以使用%20Cookie%20+%20Session%20的方式完成了登录验证,但是这种方式也存在一些问题:

  1. 这种实现方式比较简单,但不支持跨主域名。
  2. 由于服务器端需要对接大量的客户端,也就需要存放大量的%20SessionId,这样会导致服务器压力过大。
  3. 如果服务器端是一个集群,为了同步登录态,需要将%20SessionId%20同步到每一台机器上,无形中增加了服务器端维护成本。
  4. 由于%20SessionId%20存放在%20Cookie%20中,所以无法避免%20CSRF%20攻击。
2.%20认证中心

我们可以部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的%20Web%20服务,相当于一个sso服务器。

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将%20Token%20写入%20Cookie。(注意这个%20Cookie%20是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有%20Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心进行登录。由于这个操作会将认证中心的%20Cookie%20自动带过去,因此,认证中心能够根据%20Cookie%20知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标%20URL%20,并在跳转前生成一个%20Token,拼接在目标%20URL%20的后面,回传给目标应用系统。

应用系统拿到%20Token%20之后,还需要向认证中心确认下%20Token%20的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将%20Token%20写入%20Cookie,然后给本次访问放行。(这个%20Cookie%20是当前应用系统的,其他应用系统是访问不到的)当用户再次访问当前应用系统时,就会自动带上这个%20Token,应用系统验证%20Token%20发现用户已登录,于是就不会有认证中心什么事了。

总结:现在前端很多都是使用jwtToken进行加密,所以此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。 这里展开讲一下,作为一个前端,主要对这种方式的过程进行一些简单的记录说明: 由于权限问题,前端须保证用户首次访问(既没有token)的时候,尽量不看到页面,因此我们须在渲染页面之前有一层判断。通过HOC高阶组件来判断是否有token,比如判断pathname里面是否包含sso-login等字段来返回不同的页面展示,类似于下面这样:

function%20Layout({%20children,%20pathname%20})%20{//Layout为高阶组件
%20%20return%20['/sso-login'].includes(pathname)%20?%20children%20:%20<App%20init={store.init}></App>;
})

另外,还要根据网络请求后返回的code判断token是否过期,下方例子是react的写法。

Tip:react%20-%20跳转时参数带上当前url 路由页面

.config\routes.ts

%20%20{
%20%20%20%20path:%20'/',
%20%20%20%20component:%20'@/layouts/SecurityLayout',
%20%20%20%20routes:%20[
%20%20%20%20%20%20{
%20%20%20%20%20%20%20%20path:%20'/',
%20%20%20%20%20%20%20%20component:%20'@/layouts/BasicLayout',
%20%20%20%20%20%20%20%20routes:%20[
%20%20%20%20%20%20%20%20%20%20{
%20%20%20%20%20%20%20%20%20%20%20%20path:%20'/',
%20%20%20%20%20%20%20%20%20%20%20%20redirect:%20'/appManage',
%20%20%20%20%20%20%20%20%20%20},
%20%20%20%20%20%20%20%20%20%20{
%20%20%20%20%20%20%20%20%20%20%20%20path:%20'/appManage',
%20%20%20%20%20%20%20%20%20%20%20%20name:%20'appManage',
%20%20%20%20%20%20%20%20%20%20%20%20component:%20'./appList',
%20%20%20%20%20%20%20%20%20%20},
%20%20%20%20%20%20%20%20%20%20...
%20%20}

高阶组件HOC验证是否有token

src\layouts\SecurityLayout.tsx

%20%20render()%20{
%20%20%20%20const%20{%20isReady%20}%20=%20this.state;
%20%20%20%20const%20{%20children,%20loading%20}%20=%20this.props;
%20%20%20%20const%20jwt%20=%20getToken();
%20%20%20%20const%20isLogin%20=%20jwt%20&&%20jwt.expireAt%20&&%20jwt.expireAt%20>%20moment().unix();

%20%20%20%20if%20((!isLogin%20&&%20loading)%20||%20!isReady)%20{
%20%20%20%20%20%20return%20<PageLoading%20/>;
%20%20%20%20}
%20%20%20%20if%20(!isLogin%20&&%20window.location.pathname%20!==%20'/user/login')%20{
%20%20%20%20//sso-login的情况
	const%20DOMAIN%20=%20'https://sso.heiwangbatiancaishaonian.com'
%20%20%20%20const%20{%20redirect%20}%20=%20getPageQuery();
%20%20%20%20const%20queryString%20=%20stringify({
%20%20%20%20%20%20redirect:%20redirect%20||%20window.location.href,
%20%20%20%20});
%20%20%20%20const%20service%20=%20`${window.location.origin}/user/login?${queryString}`;
%20%20%20%20window.location.href%20=%20`${DOMAIN}/cas/login?service=${service}`;
%20%20%20%20	return%20null;
%20%20%20%20}
%20%20%20%20return%20children;
%20%20}

在路由中添加多一级高阶组件SecurityLayout,如果通过验证才渲染路由中页面,否则跳转。

注意HOC判断中必须先判断当前页是否为/login

http拦截器

src\utils\request.ts


request.interceptors.response.use(async%20(response:%20Response)%20=>%20{
%20%20const%20data%20=%20await%20response.clone().json();
%20%20%20%20
%20%20if%20(data.respCode%20>=%2040100%20&&%20data.respCode%20<%2040199)%20{
%20%20%20%20localStorage.clear();
%20%20%20%20const%20ssoURL%20=%20'https://sso.heiwangbatiancaishaonian.com/cas/login'
%20%20%20%20const%20{%20redirect%20}%20=%20getPageQuery();
%20%20%20%20const%20queryString%20=%20stringify({
%20%20%20%20%20%20redirect:%20redirect%20||%20window.location.href,
%20%20%20%20});
%20%20%20%20const%20service%20=%20`${window.location.origin}/user/login?${queryString}`;
%20%20%20%20window.location.href%20=%20`${ssoURL}?service=${service}`;
%20%20%20%20...

返回拦截判断code,不成功就跳转,跳转时定义redirect字段以当前路径为参数拼入重定向链接

login组件

src\pages\user\login\index.tsx


const%20Login:%20React.FC<LoginProps>%20=%20props%20=>%20{
%20%20useEffect(()%20=>%20{
%20%20%20%20const%20{%20dispatch,%20location%20}%20=%20props;
%20%20%20%20//%20node中querystring模块
%20%20%20%20const%20queryString%20=%20stringify({
%20%20%20%20%20%20redirect:%20location.query.redirect,
%20%20%20%20});
%20%20%20%20const%20service%20=%20`${window.location.origin}/user/login?${queryString}`;
%20%20%20%20//往redux里存入ticket和service,并在src/services/login.ts里将ticket进行存储
%20%20%20%20dispatch({
%20%20%20%20%20%20type:%20'login/login',
%20%20%20%20%20%20payload:%20{
%20%20%20%20%20%20%20%20ticket:%20location.query.ticket,
%20%20%20%20%20%20%20%20service:%20service,
%20%20%20%20%20%20},
%20%20%20%20});
%20%20},%20[]);

%20%20return%20(
%20%20%20%20<div%20className={styles.main}>
%20%20%20%20%20%20<PageLoading%20/>
%20%20%20%20</div>
%20%20);
};

以当前路径拼接redirect为service的字段请求token。

Tip:%20如果url上没有带ticket,前往某个系统(比如系统A)生成一个,系统A生成ticket后重定向到本页面,该例子中请求token的service参数有可能与sso的url参数中service值不同,这不是常规的操作,一般来说正确的请求参数必须与sso的url参数中service值一致,否则无法通过验证

3.%20LocalStorage%20跨域

其实单点登录的关键在于,如何让%20Session%20Id(或%20Token)在多个域中共享。但是%20Cookie%20是不支持跨主域名的,而且浏览器对%20Cookie%20的跨域限制越来越严格。

在前后端分离的情况下,完全可以不使用%20Cookie,我们可以选择将%20Session%20Id%20(或%20Token%20)保存到浏览器的%20LocalStorage%20中,让前端在每次向后端发送请求时,主动将%20LocalStorage%20的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将%20Session%20Id%20(或%20Token%20)放在响应体中传递给前端。

在这样的场景下,单点登录完全可以在前端实现。前端拿到%20Session%20Id%20(或%20Token%20)后,除了将它写入自己的%20LocalStorage%20中之外,还可以通过特殊手段将它写入多个其他域下的%20LocalStorage%20中,比如通过H5的新属性postMessage来实现跨域共享等等。

总结:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。

实现思路:当用户访问公司某系统(如product.html)时,在product中会首先加载一个iframe,iframe中可以获取存储在localStorage中的token,如果没有取到或token过期,iframe中内部将把用户将重定向到登录页,用户在此页面登录,仍将去认证系统取得token并保存在iframe页面的localStorage,实现方案大概如下所示:

  • 实现方案

    前端拿到%20Token%20后,不仅要将它写入当前域下的%20localStorage%20中,还要通过%20iframe%20+%20postMessage()%20的方式将它写入多个信任的其他域下的%20localStorage%20中,从而实现登录状态的共享

  • 实现代码

    Token 写入多个域名下的 localStorage

    const iframe = document.createElement('iframe')
    iframe.src = 'http://www.app.com/static/bridge.html'
    iframe.addEventListener('load', event => {
      iframe.contentWindow.postMessage(token, 'http://www.app.com/static/bridge.html')
    })
    document.body.append(iframe)
    
    

    iframe 加载的页面中绑定事件监听器,用来接收 Token 数据

    window.addEventListener('message', ({ data, origin, srouce }) => {
      localStorage.setItem('AUTH-TOKEN', data)
    })
    
    

    在各个应用系统请求的 Header 中携带 Token 令牌

    config.headers.common['Authorization'] = 'Bearer ' + token
    
    

Tip:Bearer 是 JWT 的认证头部信息,另外postMessage还是存在一定的兼容性问题,有兼容性要求的可以使用别的思路~

SSO 单点登录退出

目前我们已经完成了单点登录,在同一套认证中心的管理下,多个产品可以共享登录态。现在我们需要考虑退出了,即:在一个产品中退出了登录,怎么让其他的产品也都退出登录?

原理其实不难,可以在每一个产品在向认证中心验证 ticket(token) 时,其实可以顺带将自己的退出登录 api 发送到认证中心。

sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。当某个产品 c.com 退出登录时:

  1. 清空 c.com 中的登录态 Cookie
  2. 请求认证中心 sso.com 中的退出 api
  3. 认证中心遍历下发过 ticket(token) 的所有产品,并调用对应的退出 api,销毁局部会话,完成退出。
  4. sso认证中心引导用户至登录页面

单点登录前端面试话术:

主要提出以下几个关键的点,其他的自己发挥:

  1. sso提供一个所有系统重定向认证登录页面,浏览器和sso服务建立关系,分发令牌
  2. 任意一个子系统进行登录,先验证未登录,登录过了就在认证中心存储对应的令牌(ticket或token);
  3. 不同的子系统只要有一个系统开始过了,皆可使用认证中心的令牌来进行面登录访问其他子系统;
  4. 高阶组件HOC验证是否有token令牌来进行sso登录页面及相关逻辑和展示正常页面;
  5. sso单点登录退出会清空全局会话进而清空所有的局部会话;

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!
代办报建

本公司承接江浙沪报建代办施工许可证。
联系人:张经理,18321657689(微信同号)。

喜欢0发布评论

13条评论

  • 游客 发表于 2个月前

    顶!顶!顶!http://bw402.sh-jinsl.com/9/4.html

  • 游客 发表于 2个月前

    学习雷锋,好好回帖!http://test.cqyiyou.net/test/

  • 游客 发表于 2个月前

    楼上的这是啥态度呢?http://ha6ut5.hqbet7615.com

  • 游客 发表于 1个月前

    不错的帖子,值得收藏!http://cujy.lnzskj.cn

  • 游客 发表于 2周前

    今天怎么了,什么人都出来了!http://www.guangcexing.net/voddetail/pxkQnqmT.html

发表评论

  • 昵称(必填)
  • 邮箱
  • 网址