本篇文章写于2020年12月26日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
写作本篇时,笔者就职于广州洋葱集团。原标题:《实现SSO单点登录的思考》
随着公司业务的发展,子系统越来越多,实现SSO
单点登录的需求就愈加迫切。
我们一些子系统中都有使用Redis
存储Session
,这最初是为了解决应用集群部署时的Session
共享问题,却也为应用之间共享Session
提供了支持,但单靠应用之间共享Session
是无法实现单点登录的。
在单应用中,当用户登录系统后,用户的登录状态被存储在服务端的Session
中,并通过响应头的Cookie
字段将SessionId
传递给浏览器存储,后续请求服务端时,浏览器会自动在请求头加上Cookie
,所以才能保持用户的登录状态。
使用Redis
共享Session
,集群之间可以共享Session
的原理与服务重启后依然保持登录状态的原理相同。
在应用重启后,由于浏览器还是携带cookie
发起请求,如果Session
未过期,那么从Redis
获取到的Session
就能继续使用。
由此可见,虽然应用之间可通过Redis
共享Session
,但要求浏览器请求每个应用都能带上相同Cookie
才能实现单点登录。
然浏览器却不支持不同域名之间Cookie
共享,服务端也不能操控客户端浏览器访问不同域名下的站点都带上相同的SessionId
。要实现单点登录我们只能另辟蹊径。
虽然浏览器不支持不同域名之间共享Cookie
,但同一个主域名的不同子域名应用间可通过配置Cookie
为主域名方式实现Cookie
共享,前提是所有子系统都共用一个主域名。这种方案可取也不可取,短期而言可取,长期而言不可取。
如果只是实现web
应用之间相互跳转,由用户在应用a
点击按钮调整到应用b
,也可以这样实现:当用户在应用a
点击跳转应用b
时,在跳转链接上带上SessionId
,应用b
根据SessionId
读取用户信息再写入Session
。
先不讨论安全性如何,这种方式的弊端也很明显,用户不能直接在浏览器输入应用B
的域名跳转,而只能通过应用A
跳转到应用B
,要返回应用A
也只能从应用B
点击按钮跳转回应用A
。
虽然通过点击按钮方式实现应用之间互相跳转不是一个好的计策,但至少通过在跳转链接上携带SessionId
共享登录状态这个思路是可取的。
根据这个思路,我们是否可以实现不通过点击按钮方式也能让浏览器自动带上SessionId
呢?
可以,但要通过重定向实现。
当用户在系统A
登录后,直接在浏览器上修改域名访问系统B
时,系统B
检查到用户未登录后将请求重定向到系统A
,系统A
检查到请求从系统B
重定向过来,并且用户已经登录,那么可将SessionId
拼接到重定向链接上,再重定向回系统B
,系统B
获取到系统A
的SessionId
,根据SessionId
从Redis
查询用户信息,再写入系统B
的Session
中,这样就能实现自动携带SessionId
跳转。
只不过,这种方式要求每个系统都实现一遍这样的功能,并且每个系统也都要提供登录功能。
为了简化实现,以及后续的新系统不再重复实现登录功能,我们应该考虑将登录功能抽离为一个独立的应用,后续可将菜单授权等功能都迁移到该系统上,其它系统不再提供登录功能。
将登录功能抽离为独立应用之后,实现SSO
单点登录流程梳理如下:
将SSO
抽离为一个独立的应用,独立的域名,提供登录页面,要求其它应用不再提供登录页面,都必须通过SSO
登录。
其它应用在接收到请求时,首先根据session
判断是否已经登录了,如果未登录则重定向到SSO
登录页面,并且在重定向链接带上是哪个应用跳转过来的,当用户在SSO
登录成功后重定向回原来的应用。
浏览器重定向到SSO
登录页面时,浏览器会存储SSO
的cookie
,用户在SSO
登录成功后,SSO
存储用户的登录状态。SSO
生成一个token
,重定向回原应用,在重定向链接上带上token
。
原应用检查请求携带token
,这时需要访问SSO
验证token
并获取用户信息,SSO
验证成功后返回用户信息,原应用将用户信息存储到Session
中,验证成功后再重定向到首页。
如果用户此时通过在浏览器输入应用B
的域名访问应用B
,由于应用B
检查到Session
没有用户信息(未登录),于是重定向到SSO
应用。
因为用户在SSO
登录过了,重定向请求SSO
应用时浏览器会带上cookie
,所以SSO
应用发现用户已经登录,于是生成一个token
并重定向回应用B
。
应用B
接收重定向请求,从请求中获取到token
,接着访问sso
应用验证token
并获取用户信息,在获取用户信息成功后再写入Session
,最后重定向到首页。
根据梳理的流程,总结每个应用需要实现的功能:
SSO
应用:
- 提供登录功能,支持从哪重定向过来就重定向回哪去;
- 提供验证
token
接口,验证成功后响应用户信息(可以只是响应用户的id
,其它应用查询用户详细信息)。
其它应用:
- 如果未登录则跳转到
SSO
,带上登录成功后重定向调用的校验token
的链接; - 提供由
SSO
重定向调用的校验token
的接口,当校验成功后将用户信息写入Session
并重定向到前端首页。
需要注意的是,假设SSO
设置的session
过期时间为一个小时,如果用户在SSO
登录后跳转回应用A
,一个小时不操作后再跳转应用B
,此时会因为SSO
的session
已经过期导致无法同步登录状态,用户就得要重新登录,所以SSO
的session
过期时间应该根据需要合理设置,不应该设置太短。
在前后端分离的系统上实现这一流程并不容易,实际实现比本文描述的步骤还要多。我们通过封装SDK
的方式,尽可能将繁琐的步骤封装起来,让其它应用对接SSO
时仅需要依赖一个jar
包,并添加少量的配置。
SDK
通过Servlet
提供的过滤器拦截所有请求:
1、如果请求是
“/checketSsoToken”
,则说明是用户在SSO
登录成功后(浏览器重定向)跳转过来的,并且会携带token
参数。此时SDK
需要完成请求SSO
的检验token
接口,并将获取的用户信息写入Session
中,然后重定向到当前应用的前端首页。2、如果不是
“/checketSsoToken”
,则查看配置,判断当前请求是否不需要登录也可放行,如果是则放行,否则判断Session
中是否记录用户已经登录,如果未登录,则响应重定向,由前端跳转到SSO
登录。由于前后端分离,前端通过ajax
请求接口,后端判断未登录响应重定向无法真正重定向,所以要求前端拦截所有请求的响应,如果响应头有重定向标志,应从请求头获取重定向链接,然后让浏览器重定向。3、如果是退出登录请求,则先清除应用自身缓存的用户登录信息,再重定向到
SSO
退出登录。
实际对接流程如下:
1、用户在浏览器中输入应用
A
的域名,要求跳转到前端的index.html
页面;(nginx
反向代理配置实现)2、前端在首页调用一个后端接口,如获取菜单,触发校验登录(前端实现),未登录则拼接重定向链接,响应给前端,要求重定向到
SSO
登录页面(SDK
封装实现);3、用户在
SSO
登录成功后,由SSO
重定向调用应用A
的校验token
的url
。此url
在应用A
重定向到SSO
登录时作为参数拼接在URL
后面,由后端提供,前端只负责重定向;(SSO
应用实现)4、应用
A
请求SSO
的校验token
接口,并将响应的用户信息写入session
,重定向回前端首页。(SDK
封装实现)
最后留下一道思考题:如何同步退出登录状态?
当用户在应用A
退出登录时,只有应用A
和SSO
知道用户退出登录了,但其它应用却不得而知。
并不是实现不了,而是在寻找最优的方式。最简单的方式就是除SSO
之后,将其它应用的Session
过期时间配置尽可能短,宁愿多重定向几次。
最后,由于每个应用都用了Shiro
实现接口权限校验,也用了Shiro
的注解,所以权限校验的实现,我们在SDK
适配了Shiro
的注解,但完全弃用了Shiro
。