随着公司业务的发展,子系统越来越多,实现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获取当前登录用户信息的接口。
其它应用:
未登录则重定向跳转到SSO,在跳转链接上带上登录成功后重定向调用的接口;
提供给SSO重定向调用的接口,用于接收SSO传递的token,根据token从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的“/checketSsoToken”。此url在应用A重定向到SSO登录时作为参数拼接在URL后面,由后端提供,前端只负责重定向;(SSO应用实现)
4、应用A请求SSO的校验token接口,并将响应的用户信息写入session,重定向回前端首页。(SDK封装实现)
需要注意的是,假设SSO设置的session过期时间为一个小时,如果用户在SSO登录后跳转回应用A,一个小时不操作后再跳转应用B,此时会因为SSO的session已经过期导致无法同步登录状态,用户就得要重新登录,所以SSO的session过期时间应该根据需要合理设置,不应该设置太短。
最后留下一道思考题:如何同步退出登录状态?
当用户在应用A退出登录时,只有应用A和SSO知道用户退出登录了,但其它应用却不得而知。
最简单的方式就是除SSO之后,将其它应用的Session过期时间配置尽可能短。又或者每次打开应用的首页都先跳转到SSO,如果已经登录,自然会重定向回来,这一个步骤对用户来说是透明的。
最后,由于每个应用都用了Shiro实现接口权限校验,也用了Shiro的注解,所以权限校验的实现,我们在SDK适配了Shiro的注解,但完全弃用了Shiro。
本文转载自微信公众号「Java艺术」,可以通过以下二维码关注。转载本文请联系Java艺术公众号。