在早期,项目规模不大的时候,企业中存在的系统不多,通常为1或者2个。每个系统都有自己独立的登陆模块,这样用户进行登陆也不是特别麻烦,分别进行登陆就可以了。但是随着企业规模不断变大,随之而然的系统模块也越来越多,而每个模块都有自己独立的登陆,那么用户就会有很多登陆账号,想要进入系统都得进行单独登陆,这不然是最痛苦的。 那么能不能只在一个系统进行登陆成功之后,在其他系统就不用再登陆了呢?就行我在淘宝网站进行登陆之后,点击链接进入天猫商城一样,不用再重新进行登陆。解决办法是有的,那就是使用单点登陆方案解决。方案有了,不同实现的技术就会出现。下面介绍的是许雪里老师编写开源出来的xxl-sso框架。 单点登陆英文全称Single Sign On ,简称就是SSO。它的解释就是:在多个应用系统中,只需要在一个系统中完成登陆,就可以访问其他相互信任的应用系统。 之前是每一个系统都有单独的登陆认证模块,现在是将登陆功能统一的由SSO进行登陆认证,其它的应用模块没有登陆功能。
Xxl-sso是一个分布式单点登陆框架。只需要登陆一次就可以访问所有相互信任的应用系统。下面先介绍一下它的特性有哪些:
简洁:API直观简洁,可快速上手 轻量级:环境依赖小,部署与接入成本较低 单点登录:只需要登录一次就可以访问所有相互信任的应用系统 分布式:接入SSO认证中心的应用,支持分布式部署 HA:Server端与Client端,均支持集群部署,提高系统可用性 跨域:支持跨域应用接入SSO认证中心 Cookie+Token均支持:支持基于Cookie和基于Token两种接入方式,并均提供Sample项目 Web+APP均支持:支持Web和APP接入 实时性:系统登陆、注销状态,全部Server与Client端实时共享 CS结构:基于CS结构,包括Server”认证中心”与Client”受保护应用” 记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期 路径排除:支持自定义多个排除路径,支持Ant表达式,用于排除SSO客户端不需要过滤的路径项目下载地址: https://github.com/xuxueli/xxl-sso
包含三个模块 xxl-sso-server:中央认证服务,支持集群 xxl-sso-core:Client端依赖,主要作用为路径的排除,哪些路径不需要登陆就可以访问的、登陆时token的认证检查等 xxl-sso-samples:单点登陆Client端接入示例项目 xxl-sso-web-sample-springboot:基于Cookie接入方式,供用户浏览器访问,springboot版本 xxl-sso-token-sample-springboot:基于Token接入方式,常用于无法使用Cookie的场景使用,如APP、Cookie被禁用等,springboot版本 环境 JDK:1.7+ Redis:4.0+架构图 相关配置文件介绍 xxl-sso-server认证中心(SSO Server) 配置文件位置:application.properties ### xxl-sso redis 地址: 如 "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";多地址逗号分隔 xxl.sso.redis.address=redis://127.0.0.1:6379 登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期 xxl.sso.redis.expire.minite=1440密码配置:可查看com.xxl.sso.core.util下的JedisUtil类中获取ShardedJedis实例代码 2. xxl-sso-web-sample-springboot(应用client) 引入xxl-sso-core核心依赖包
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-sso-core</artifactId> <version>${最新稳定版}</version> </dependency>配置xxlSsoFilter 配置文件位置:application.properties
### xxl-sso ### Sso server认证中心地址 xxl.sso.server=http://xxlssoserver.com:8080/xxl-sso-server ### 注销登陆path xxl.sso.logout.path=/logout ### 路径排除path,允许设置多个,且支持Ant表达式。用于排除SSO客户端不需要过滤的路径 xxl-sso.excluded.paths= // redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated xxl.sso.redis.address=redis://127.0.0.1:6379 修改hosts文件,模拟真实环境 127.0.0.1 xxlssoserver.com 127.0.0.1 xxlssoclient1.com 127.0.0.1 xxlssoclient2.com如果在hosts文件中添加了以上内容,不生效,解决方法: 打开命令行窗口: ipconfig /displaydns 查看配置的dns是否存在 ipconfig /flushdns刷新dns配置
项目启动 分别运行“xxl-sso-server”“xxl-sso-web-sample-springboot”项目相应访问测试地址
1、SSO认证中心地址: http://xxlssoserver.com:8080/xxl-sso-server 2、Client1应用地址: http://xxlssoclient1.com:8081/xxl-sso-token-sample-springboot/ 3、Client2应用地址: http://xxlssoclient2.com:8081/xxl-sso-token-sample-springboot/ 源码跟踪 访问Client1应用地址,会经过XxlSsoWebFilter拦截。XxlSsoWebFilter存在于核心依赖包中,应用中配置如下: 在XxlSsoWebFilter过滤器中,先进行init初始化操作,初始化内容为在Client1应用中配置的相关信息。然后走doFilter方法,实现登陆检查。 doFilter方法具体代码如下: @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; // (1) 请求路径 String servletPath = req.getServletPath(); //(2) 路径排除 if (excludedPaths!=null && excludedPaths.trim().length()>0) { for (String excludedPath:excludedPaths.split(",")) { String uriPattern = excludedPath.trim(); // 支持ANT表达式 if (antPathMatcher.match(uriPattern, servletPath)) { chain.doFilter(request, response); return; } } } // (3) 登出 if (logoutPath!=null && logoutPath.trim().length()>0 && logoutPath.equals(servletPath)) { // remove cookie SsoWebLoginHelper.removeSessionIdByCookie(req, res); // redirect logout String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT); res.sendRedirect(logoutPageUrl); return; } // (4) 登陆检查 XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res); // (5) 用户信息不存在时 if (xxlUser == null) { String header = req.getHeader("content-type"); boolean isJson= header!=null && header.contains("json"); if (isJson) { //(6) json消息 res.setContentType("application/json;charset=utf-8"); res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}"); return; } else { // (7) 访问源地址 http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/ String link = req.getRequestURL().toString(); // (8) 重定向地址 http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/ String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN) + "?" + Conf.REDIRECT_URL + "=" + link; res.sendRedirect(loginPageUrl); return; } } // ser sso user request.setAttribute(Conf.SSO_USER, xxlUser); // (9) 过滤器放行 chain.doFilter(request, response); return; }编号注释(1-9): (1)-(3):主要做路径排除和用户登出的操作。 (4):登陆用户信息检查(重点部分),下面会进行代码跟踪。 (5):如果没有用户信息的处理。 (6-9):相关信息注释,下面会使用到。
接下来,我们看些编号(4)做的具体处理逻辑 调用SsoWebLoginHelper中的loginCheck方法 又会调用SsoTokenLoginHelper中的lgoinCheck方法,代码如下: public static XxlSsoUser loginCheck(String sessionId){ // 解析key sessionId生成规则 userId+"_"+version String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId); if (storeKey == null) { return null; } //根据key从redis中获取用户信息 XxlSsoUser xxlUser = SsoLoginStore.get(storeKey); if (xxlUser != null) { String version = SsoSessionIdHelper.parseVersion(sessionId); if (xxlUser.getVersion().equals(version)) { // 判断时间是否过半,如果是 自动刷新 if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinite()/2) { xxlUser.setExpireFreshTime(System.currentTimeMillis()); SsoLoginStore.put(storeKey, xxlUser); } return xxlUser; } } return null; } /** * login check * * @param request * @return */ public static XxlSsoUser loginCheck(HttpServletRequest request){ String headerSessionId = request.getHeader(Conf.SSO_SESSIONID); return loginCheck(headerSessionId); } 如果(4)走完,XxlSsoUser为空 判断是否是json请求,此时请求不是json的。重定向到(8)SSO Server项目进行认证: 认证地址:http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1:8081/xxl-sso-web-sample-springboot/ 将重定向的地址存放到登陆隐藏域中 登陆 点击登陆,请求/doLogin @RequestMapping("/doLogin") public String doLogin(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes, String username, String password, String ifRemember) { //判断是否勾选了记住我的功能 boolean ifRem = (ifRemember!=null&&"on".equals(ifRemember))?true:false; // 查询用户逻辑 ReturnT<UserInfo> result = userService.findUser(username, password); if (result.getCode() != ReturnT.SUCCESS_CODE) { redirectAttributes.addAttribute("errorMsg", result.getMsg()); redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); return "redirect:/login"; } // 模拟用户信息 XxlSsoUser xxlUser = new XxlSsoUser(); xxlUser.setUserid(String.valueOf(result.getData().getUserid())); xxlUser.setUsername(result.getData().getUsername()); xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", "")); xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite()); xxlUser.setExpireFreshTime(System.currentTimeMillis()); // 生成sessionId,生成规则:userId+"_"+version String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser); // 存储到redis和cookie中 SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem); // 获取隐藏域中存放的登陆成功之后的地址 String redirectUrl = request.getParameter(Conf.REDIRECT_URL); if (redirectUrl!=null && redirectUrl.trim().length()>0) { String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId; return "redirect:" + redirectUrlFinal; } else { return "redirect:/"; } }携带sessionId重定向到源请求资源地址:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/,此时又会经过XxlSsoWebFilter过滤器 走doFilter方法 调用loginCheck方法检查验证,并写入cookie
到这里xxlssoclient1.com登陆成功
请求Client2应用地址 同样会经过XxlSsoWebFilter,调用SsoWebLoginHelper.loginCheck方法校验用户信息。最后会被重定向到Sso Server 此时SSO Server怎么知道client2不用在进行登陆了呢?看SSO Server项目中代码 @RequestMapping(Conf.SSO_LOGIN) public String login(Model model, HttpServletRequest request, HttpServletResponse response) { // 做登陆检查,此时获取cookieSessionid是从xxlssoserver.com域名下获取的,一定存在用户信息,此前在client1系统中已经登陆过了。 XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response); if (xxlUser != null) { // success redirect String redirectUrl = request.getParameter(Conf.REDIRECT_URL); if (redirectUrl!=null && redirectUrl.trim().length()>0) { String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request); String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;; // 重定向到client2应用地址,携带sessionId return "redirect:" + redirectUrlFinal; } else { return "redirect:/"; } } model.addAttribute("errorMsg", request.getParameter("errorMsg")); model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL)); return "login"; }同client1重定向方式相同,会经过XxlSsoWebFilter过滤器 走doFilter方法 调用loginCheck方法检查验证,并写入cookie 至此实现了在Client1系统中完成登陆后,访问Client2系统,不再进行登陆操作。