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
。
https://mp.weixin.qq.com/s/C_5KjL3jeUiLhzVjX9Abeg