随着公司业务的发展,子系统越来越多,实现SSO单点登录的需求也越来越迫切。我们的一些子系统使用Redis来存储会话。这本来是为了解决部署应用集群时的session共享问题,但同时也提供了对应用间共享session的支持。但是,无法实现单点登录。在单体应用中,当用户登录系统时,用户登录状态保存在服务器端的session中,通过响应头的Cookie字段将SessionId传递给浏览器保存。在header中加入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登录页面时,浏览器将存储SSOcookie。用户通过SSO登录成功后,SSO会存储用户的登录状态。SSO生成令牌,重定向回原始应用程序,并在重定向链接上携带令牌。原始应用程序检查请求是否携带令牌。这时候就需要访问SSO验证token,获取用户信息。单点登录验证成功后,返回用户信息。原应用将用户信息保存在Session中,验证成功后重定向到首页。如果用户此时在浏览器中输入应用B的域名访问应用B,应用B检查会话没有用户信息(未登录),因此重定向到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,1小时无操作后再跳转到应用B,则会由于SSO会话过期,无法同步登录状态,用户不得不重新登录。因此,SSO的会话过期时间应根据需要合理设置,不宜设置过短。最后还有一个思考问题:如何同步注销状态?当用户在应用A注销时,只有应用A和SSO知道用户已经注销,其他应用不知道。最简单的方法是在SSO后将其他应用程序的会话过期时间配置得尽可能短。或者每次打开应用的首页,都会先跳转到SSO。如果您已经登录,您自然会被重定向回来。此步骤对用户是透明的。最后,由于每个应用都使用Shiro实现接口权限校验,同时也使用Shiro的注解,所以我们在SDK中适配Shiro的注解来实现权限校验,而完全放弃Shiro。本文转载自微信公众号“爪哇艺术”,可通过以下二维码关注。转载本文请联系爪哇艺术公众号。