java实现完全跨域SSO单点登录

java实现SSO

什么是SSO

SSO(Single Sign On)单点登录是实现多个系统之间统一登录的验证系统,简单来说就是:有A,B,C三个系统,在A处登录过后,再访问B系统,B系统就已经处于了登录状态,C系统也是一样。举个生活中栗子:你同时打开天猫和淘宝,都进入login界面,都要求你登录的,现在你在淘宝处登录后,直接在天猫处刷新,你会发现,你已经登录了,而且就是你在淘宝上登录的用户。说明他们实现了SSO,并且持有相同的信息。

当然这个特性意味着它的使用场景是:同一公司下的不同子系统,因为对于SSO来说,每一个子系统拥有的信息都一样,是用户的全部信息,如果是不同公司,那这肯定不合适。现在的天猫和淘宝就是这样的一套SSO。

实现思想

SSO简单来说就是一句话:一处登录,全部访问。
现在有两个系统分别是:a.comb.com,我们要实现他们的SSO,那么我们就需要一个统一验证中心sso.com,我们所有的登录和身份验证都在sso.com中操作。看图看传统登录方式和SSO方式的差别如下:
传统与SSO方式对比

我们需要将统一信息存在cookie中。

登录部分:

在用户第一次访问a.com时,到达a.com的服务器,服务器请求sso.com/ssocheck验证,验证失败,a.com的服务器到达login界面,用户在login界面输入用户名和密码,到达a.com的服务器,请求sso.com/login验证,验证通过生成token(包括用户登录信息),然后携带token和所有子系统路径返回a.com的服务器,a.com的服务器到达首页,同时请求自己和所有子系统的addcookie方法,将token添加到自己的cookie中。
在用户访问b.com时,同样向sso.com/check发出验证cookie请求,sso验证token,验证成功返回到b.com的服务器,然后到达b的首页显示登录成功。

退出部分:

用户点击a.com的退出按钮,访问sso.com/loginout,然后获得所有子系统信息,请求所有子系统clearcookie方法,并重定向到login界面。

跨域部分:我使用的是ajax的jsonp方式。

看下登录(退出就不看了,登录写出来后退出就很简单了)的流程图:
登录流程图

代码实现

按流程展示代码:
用户访问a.com,用户先验证cookie到达a.com/ssocheck


    /**
     * 
     * @return 响应界面:login/index
     */
    @GetMapping("/ssocheck")
    public ModelAndView checkCookies (HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if ("jian".equals(cookie.getName())) { //统一登录cookie为jian,如果存在就认证
                    log.info("cookie 存在,开始验证");
                    HttpUtil httpUtil = new HttpUtil("http://sso.com/sso/authcookies", Method.GET);
                    String result = httpUtil.send(cookie.getName(), cookie.getValue());
                    boolean authBoo  = Boolean.valueOf(result);
                    if (authBoo) {
                        log.info("验证通过");
                        return new ModelAndView("public/index");
                    }
                    break;
                }
            }
        }
        return new ModelAndView("index");
    }

在判断中,如果a.com中有名为jian的cookie,那么就去sso.com/sso/authcookies去认证cookievalue,那么在sso.com中方法是这样的:


    /**
     * 验证cookie是否通过
     * @param cookieName cookie名称
     * @param cookieValue cookie内容
     * @return 是否认证成功
     */
    @GetMapping("/authcookies")
    public boolean checkAuthCookies (String cookieName, String cookieValue) {
        boolean isUpdate = new JwtUtil(null,cookieValue).freeJwt();
        if ("jian".equals(cookieName) && "ok".equals(cookieValue)) {
            log.info("cookie验证通过");
            return true;
        }
        return false;
    }


这里用到了HttpUtil类,这是我自己封装的,先说正事,这个工具类下面再放它,不要打扰了主线。如果认证cookie通过,那么说明已经在别的系统处登录过了,然后a.com就返回到首页,如果认证失败,a.com就到达登录页面,在我例子这是分别是public/index和index界面。
登录界面就不亮了,很简单,就两个输入框,输入用户名和密码,然后提交到a.com/login,然后看下这个方法


    /**
     * 登录
     * @param username 用户名
     * @param password 密码
     * @return index/login
     */
    @PostMapping("/login")
    public ModelAndView doLogin (String username, String password) {
        if (username != null && !"".equals(username) &&
                    password != null && !"".equals(password) ) {
            HttpUtil httpUtil = new HttpUtil("http://sso.com/sso/", "POST");
            Result result = httpUtil.sendLogin(username,password);
            //如果验证通过,就携带所有子系统域名返回首页
            int isLogin = result.getResultCode().getCode();
            if (isLogin == 1) {
                @SuppressWarnings("all")
                Map<String,String> param = (Map<String, String>) result.getData();
                return new ModelAndView("public/index","sendparam",param);
            }
        }
        return new ModelAndView("index");
    }

a.com处请求sso.com/sso验证


    /**
     * 统一处理login请求
     * @param username 用户名
     * @param password 密码
     */
    @PostMapping
    public Result<Map<String,Object>> checkLogin (String username, String password) {
        log.info("统一登录校验");
        TbUser user = userService.login(username, password);
        if (user != null) {
            //封装参数
            Map<String, Object> param = new HashMap<>();
            //获得所有子系统域名信息
            List<TbDomain> domains = domainService.selectAll();
            List<String> domainUrl = new ArrayList<>(domains.size());
            domains.forEach(domain->{
                domainUrl.add(domain.getDomain()+"/addcookie");
            });
            //生成jwt,加密用户信息
            String cookieName = "jian";
            String cookieValue = new JwtUtil(user.toString(),null).creatJwt();
            param.put("cookieurl",domainUrl);
            param.put("cookieName", cookieName);
            param.put("cookieValue",cookieValue);
            Result<Map<String, Object>> result = new Result<>(ResultCodeEnum.AUTHSUCCESS);
            result.setData(param);
            return result;
        }
        return new Result<>(ResultCodeEnum.UNAUTHORIZEd,"账号或密码错误");
    }

在这里如果验证失败就返回账号或密码错误,如果验证通过,就得到所有域名,然后加密当前用户信息,用到了jwt(json web token,不做过多讲解),然后返回a.com。在a.com发现验证通过就到达首页,验证失败继续到达登录,在首页要使用ajax循环访问所有子系统,将cookie信息添加到所有子系统下。
我的模板引擎使用的是thymeleaf,首页js如下:


<script th:inline="javascript">
    /*<![CDATA[*/

    $(function () {
        //后台的所有域名
        var params = [[${sendparam}]];
        if (params == null) {
            return;//中断执行
        }
        var arrDomain = params.cookieurl;
        jQuery.each(arrDomain, function () {  // this 指定值
            //循环访问
            $.ajax({
                url: this + "?cookieName=" + params.cookieName + "&cookieValue=" + params.cookieValue,
                type: "get",
                dataType: "jsonp" //指定服务器返回的数据类型
            });
        });
    })
    /*]]>*/
</script>

然后在此访问所有子系统的/addcookie方法,这里涉及到跨域,可以看到,我跨域使用的是ajax的json方式,中间还遇到了一些异常,总之解决掉了。

看下a.com/addcookie方法

    /**
     *
     * @param cookieName cookie名称
     * @param cookieValue cookie值
     * @param response 响应
     */
    @GetMapping("/addcookie")
    public void addCookies (String cookieName, String cookieValue, HttpServletResponse response) {
        log.info("添加cookie");
        Cookie cookie = new Cookie(cookieName,cookieValue);
        cookie.setPath("/");
        cookie.setMaxAge(3600);
        cookie.setHttpOnly(true);
        response.addCookie(cookie);
    }

然后这时候查看下浏览器的cookie就会发现已经为它写上了,访问b.com/ssocheck就会直接通过,b.com下也被写了cookie。

看下退出,退出就简便多了。
首先在a.com首页点击退出按钮,然后触发js,访问sso.com/logout方法。


//退出登录,清空所有子系统的cookie
    $("#loginout").click (function (event) {
        $.ajax({
            url: "http://sso.com/sso/loginout",
            type: "get",
            jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(默认为:callback)
            jsonpCallback:"success_jsonpCallback",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名
            dataType: "jsonp", //指定服务器返回的数据类型
            success: function (data) {
                window.location.href="/loginout";
                eachUrl(data);//循环清理掉所有子系统cookie
            },error:function (data) {
                console.log(data.jqXHR+" "+data.status+" "+data.error);
            }
        });
    });

    function eachUrl(arrDomain) {
        jQuery.each(arrDomain, function () {  // this 指定值
            //循环访问
            $.ajax({
                url: this,
                type: "get",
                dataType: "jsonp" //指定服务器返回的数据类型
            });
        });
    }

会去访问sso.com/loginout方法,拿到所有域名清除cookie方法。


    /**
     * 添加需要清除的cookie
     */
    @GetMapping("/loginout")
    public String loginOut (HttpServletRequest request) {
        String callbackFuncation = request.getParameter("callback");
        log.info("start clear");
        List<TbDomain> domains = domainService.selectAll();
        List<String> domainUrl = new ArrayList<>(domains.size());
        domains.forEach(domain->{
            domainUrl.add(domain.getDomain()+"/clear");
        });
        String resultMsg = JSON.toJSONString(domainUrl);
        return callbackFuncation+"("+resultMsg+")";
    }

然后拿到后,首先会自己跳回到登录界面,然后再去请求其他子系统的清除cookie方法,防止请求时间过长无法给用户响应。看下清除cookie方法。


    /**
     * 清除掉cookie
     * @param request 请求
     * @param response 响应
     */
    @GetMapping("/clear")
    public void clear (HttpServletRequest request,HttpServletResponse response) {
        //获得域名
        log.info("clear掉ip为:"+request.getRemoteHost()+"的cookie");
        Cookie [] cookies = request.getCookies();
        for (Cookie cookie: cookies) {
            if ("wlgzs".equals(cookie.getName())) {
                cookie.setValue(null);
                cookie.setMaxAge(0);
                response.addCookie(cookie);
            }
        }
    }

然后再去访问刚才还能访问的b.com就会发现验证失败返回到登录界面,它的cookie也被清除了。

至此:SSO的流程就分享完了,至于其中的http,jwt工具类都是小东西,相比本文不是重点,就不贴出代码了。
本文所有代码在github上已经发布。github地址:https://github.com/zhangjingao/sso

猜你喜欢

转载自blog.csdn.net/zhangjingao/article/details/81735041
今日推荐