天天看点

sso单点登陆实现过程汇总记录前言

前言

    在早期,项目规模不大的时候,企业中存在的系统不多,通常为1或者2个。每个系统都有自己独立的登陆模块,这样用户进行登陆也不是特别麻烦,分别进行登陆就可以了。但是随着企业规模不断变大,随之而然的系统模块也越来越多,而每个模块都有自己独立的登陆,那么用户就会有很多登陆账号,想要进入系统都得进行单独登陆,这不然是最痛苦的。

    那么能不能只在一个系统进行登陆成功之后,在其他系统就不用再登陆了呢?就行我在淘宝网站进行登陆之后,点击链接进入天猫商城一样,不用再重新进行登陆。解决办法是有的,那就是使用单点登陆方案解决。方案有了,不同实现的技术就会出现。下面介绍的是许雪里老师编写开源出来的xxl-sso框架。

    单点登陆英文全称Single Sign On ,简称就是SSO。它的解释就是:在多个应用系统中,只需要在一个系统中完成登陆,就可以访问其他相互信任的应用系统。

    之前是每一个系统都有单独的登陆认证模块,现在是将登陆功能统一的由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客户端不需要过滤的路径
           

xxl-sso框架结构

项目下载地址: https://github.com/xuxueli/xxl-sso

  • 包含三个模块
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+

  • 架构图
    sso单点登陆实现过程汇总记录前言
  • 相关配置文件介绍
  1. 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实例代码

sso单点登陆实现过程汇总记录前言

2. xxl-sso-web-sample-springboot(应用client)

引入xxl-sso-core核心依赖包

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-sso-core</artifactId>
    <version>${最新稳定版}</version>
</dependency>
           

配置xxlSsoFilter

sso单点登陆实现过程汇总记录前言

配置文件位置: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/
           
  • 源码跟踪
  1. 访问Client1应用地址,会经过XxlSsoWebFilter拦截。XxlSsoWebFilter存在于核心依赖包中,应用中配置如下:
    sso单点登陆实现过程汇总记录前言
  2. 在XxlSsoWebFilter过滤器中,先进行init初始化操作,初始化内容为在Client1应用中配置的相关信息。然后走doFilter方法,实现登陆检查。
    sso单点登陆实现过程汇总记录前言
  3. 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):相关信息注释,下面会使用到。

  1. 接下来,我们看些编号(4)做的具体处理逻辑

    调用SsoWebLoginHelper中的loginCheck方法

    sso单点登陆实现过程汇总记录前言
    又会调用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);
    }
           
  1. 如果(4)走完,XxlSsoUser为空
    sso单点登陆实现过程汇总记录前言

    判断是否是json请求,此时请求不是json的。重定向到(8)SSO Server项目进行认证:

    认证地址:http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1:8081/xxl-sso-web-sample-springboot/

    sso单点登陆实现过程汇总记录前言
    将重定向的地址存放到登陆隐藏域中
    sso单点登陆实现过程汇总记录前言
  2. 登陆

    点击登陆,请求/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方法

sso单点登陆实现过程汇总记录前言

调用loginCheck方法检查验证,并写入cookie

sso单点登陆实现过程汇总记录前言

到这里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方法

sso单点登陆实现过程汇总记录前言

调用loginCheck方法检查验证,并写入cookie

sso单点登陆实现过程汇总记录前言

至此实现了在Client1系统中完成登陆后,访问Client2系统,不再进行登陆操作。