天天看点

java实现 SSO 单点登录(最终版)--补充完全跨域SSO前言同父域单点登录在这个版本上补充完全跨域SSO内容

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhangjingao/article/details/89052764

前言

  前面我写了一篇文章,java实现完全跨域SSO单点登录,最后我会比较两种方案。

  那篇文章主要说明完全跨域SSO单点登录的实现,但是我最终并没有使用那篇,当然,那篇完全可以实现SSO跨域,但是那篇有一些不太优雅的地方,我综合我的场景等各方面考虑,最终选择了我下面的这个方案。因为那篇并没有被选用,所以代码大家可以随意看,但是下面这个方案因为代码已经在使用,所以不太方便分享代码,见谅。大家有什么问题,欢迎留言或者qq私我,如果是qq(1763608200)私我,建议加验证信息:sso探讨(内心os: 不加验证信息我不会通过的,最近信息泄露太严重了,好多头像是杂七杂八的不知道是卖茶还是干啥的老加我 )

同父域单点登录

什么是SSO

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

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

这套方案我们要实现的效果

  假设此时有A,B,C三套系统,他们有相同的顶级域名,就是说他们是同一个域名的子域名,此处使用a.xxx.com,b.xxx.com,c.xxx.com来表示,有一个独立的验证中心,使用sso.xxx.com表示。当我们在a.xxx.com下登录后,我当前的浏览器访问b.xxx.com依然能直接访问,并且已经处于登录状态。当我在A系统中注销登录时,B系统也会被注销登录。但是如果是b.yyy.com在该方案下就不支持了。

具体流程

java实现 SSO 单点登录(最终版)--补充完全跨域SSO前言同父域单点登录在这个版本上补充完全跨域SSO内容

  初始A,B,C全部处于未登录状态,没有令牌(tokenid)。

  1、用户访问A系统,没有令牌,A系统验证未登录,直接返回A的登录界面;

  2、A输入账号密码进行登录请求到SSO中(附带重定向url);

  3、然后SSO验证用户信息正确,生成身份令牌,将身份令牌转为jwt,将用户信息使用AES加密,将jwt身份令牌和用户信息作为键值对存进redis,时间设为30分钟,将身份令牌种进cookie,重定向回携带的url(到达了子系统);

  4、子系统发现有身份令牌,发送请求到达SSO验证令牌真伪;

  5、SSO收到验证请求,验证通过,返回AES加密的用户信息;

  6、子系统收到通过信息,将用户信息解密,存进session一分钟(token),跳转页面至首页或用户请求页;

  7、一分钟失效后或者子系统访问时,发现有令牌但session中无信息,再次重复4,5,6。

解释几个疑问点

1、sso中redis存储的是全局会话,而每个子系统中session存储的是临时会话。

2、为什么临时会话定义一分钟?

  定义一分钟,就是为了当A系统退出后,注销了本地临时会话,再到sso注销全局会话,一分钟的话即使不去注销B系统的临时会话,也很快就会消失,这个时间可以忽略,再者一分钟可以保证子系统和sso实时保证用户信息的统一,假设此时该用户修改了个人信息,其他系统也能保证尽快得到最新的信息。

3、为什么sso中要用redis存储身份令牌呢?为什么不用session呢?

  首先各个子系统验证身份令牌时是由各个子系统发出的,不一定是登录的那个域名了,所以无法保证session还能跟登录的那个session一致,所以使用redis保存,这样身份令牌和sso的redis之间也模拟出来一个全局会话关系出来。

4、为什么要用JWT呢?

  是为了实现数字签名,如果黑客窃取到身份令牌,暴力枚举令牌,是有机会泄露的,使用JWT后,sso可以验证这是否被篡改。

5、为什么要重定向回子系统?

  首先A系统登录时是在自己的登录界面,然后直接提交到sso验证中心,这个时候这个请求是浏览器和sso建立的联系,跟子系统没关系,直接返回或者转发什么的是没办法回到子系统的。

6、为什么登录要直接提交到SSO验证中心?(内心os:我感觉我快成了杠精,可能我适合去工地,2333 )

  因为身份令牌要保存在浏览器客户端,保存在cookie中,也就是说sso此时要拿到浏览器请求才能给浏览器的父域种下cookie,如果是通过子系统再发出请求,那样是种不了cookie的,当然如果是登录请求提交到子系统,子系统发出验证,由子系统去种cookie也是可以的,但是这样的话你内聚就降低了很多,耦合度也高了,子系统原本不用管的登录的事也要管了,sso原本要全管的登录被别人干了一部分。

简单看下代码

SSO的统一登录代码

/**
     * 统一验证登录中心
     * @param username 用户名
     * @param password 密码
     */
    @PostMapping
    public void checkLogin(String username, String password, String returnUrl,HttpServletResponse response) {
        TbUser user = userService.login(username, password);
        log.info("user: "+user);
        String jwtValue = null;
        if (user != null) {
            /*
             * 获得uuid,作为tokenId(TGC)
             * 将tokenId存进jedis,返回客户端以jwt存储的tokenId,
             * 即使tokenId被截取也无所谓
             */
            String tokenId = UUID.randomUUID().toString();
            //生成jwt
            jwtValue = new JwtUtil(tokenId, null).creatJwt();
            log.info("tokenId: " + tokenId+" jwt: "+jwtValue);
            if (jwtValue != null) {
                Jedis jedis = jedisPool.getResource();
                jedis.set(tokenId, user.toString());
                jedis.expire(tokenId,1800);
                log.info("查看key的剩余生存时间:"+jedis.ttl(tokenId));
            }
        }
        //将jwt加密的TGC存进cookie
        Cookie cookie = new Cookie("tokenId", jwtValue);
        cookie.setPath("/"); //设置根域名
        cookie.setHttpOnly(true);
        response.addCookie(cookie);
        try {
            response.sendRedirect(returnUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
           

子系统使用身份令牌验证

/**
     * 验证wlId
     * 用jedis的原因是不用的话无法从所有session会话中找到sessionid为tokenid的会话
     * @param wlId wlId
     * @return  如果失效返回null,反之返回aes加密的用户信息
     */
    @GetMapping
    public String tokenCheck (String wlId) {
        //从jedis中获得token
        Jedis jedis = jedisPool.getResource();
        String user = null;
        if (wlId != null) {
            wlId = new JwtUtil(null, wlId).decryptJwt();
            user = jedis.get(wlId);
            jedis.expire(wlId,1800);
            log.info("tokencheck查看key的剩余生存时间:"+jedis.ttl(wlId));
        }
        return user;
    }

           

退出登录的方法

/**
     * 清除全局会话,子系统跳转到回到登录页的方法
     * @param wlId 身份令牌
     */
    @GetMapping("/loginout")
    public void loginOut(String wlId) {
        log.info("clear token");
        Jedis jedis = jedisPool.getResource();
        jedis.del(wlId);
    }


           

与我另一篇完全跨域sso对比

  首先附上篇文章链接:java实现完全跨域SSO单点登录

  1、在以后你们的系统可能短时间内不会使用多个根域名的话,建议使用该版本或者修改该版本。

  2、两者都可以实现单点登录,我已经自己试验过了。并且另一个版本有github代码提供。

  3、上篇是完全跨域SSO,即使以后使用不同的域名,完全没问题。该版本只适用于同父域单点登录。

  4、上篇虽然是完全跨域sso,但是sso需要多维护一个子系统表,用来记录向子系统添加cookie的链接,该篇不需要。

  5、当以后用户量大,业务拓展的时候,第一个登录的子系统不仅要使用更多的时间验证用户是否正确,并且还要花时间对那么多个子系统进行重定向种cookie,虽然可以先跳转到用户首页再去请求,但是也会造成第一个登陆的登录速度慢一些,用户体验可能就不那么好。

在这个版本上补充完全跨域SSO内容

这个补充是我后来想的,我并未实践,但是我觉得可行,欢迎留言或加q探讨

先看下草图

java实现 SSO 单点登录(最终版)--补充完全跨域SSO前言同父域单点登录在这个版本上补充完全跨域SSO内容

有a,b两个系统,域名为a.com,b.com。然后sso为 sso.com。首先都处于未登录状态a系统和b系统都是自己独立的登录界面。

1、a首先去请求,然后a的server发现未登录,这个时候首先重定向sso.checkTokenId,这样sso发现cookie下确实没有种下身份令牌这样就重定向回a.login界面

2、a在登录界面提交账号密码到达sso.login方法,sso验证信息正确,那么就为sso.com的cookie种下身份令牌这个就是全局会话,重定向回最初的访问链接,并携带身份令牌,在a处生成了局部会话。

3、现在客户端再去访问b.com下的页面,这个时候b.server发现没有局部会话,未登录,这个时候首先重定向到sso.checkTokenId,sso验证是否有全局的身份令牌

4、sso验证有身份令牌,那么就返回b最初的访问链接,携带身份令牌,在b处生成局部会话,b处也就成了已登录

5、sso验证没有身份令牌,那么就重新到b.com下的b.login界面,重复2步骤,最终生成全局会话和局部会话

涉及到的问题

1、每次重定向携带信息,这个时候可以使用302重定向跳转,可以实现get重定向并携带参数

2、sso系统加了一个checkTokenId方法就是精华,这样每次判断是否有会话,就可以重定向到此获得sso.com的cookie,这样就可以得到全局的身份令牌

3、此时各个子系统使用的是不同的登录界面,如果想要使用统一的登录界面,依然要采用本设计,因为如果只是单纯的靠统一的登录界面来获得sso.com的全局会话,这样做不到用户无感知,加一个方法就是为了不做页面跳转。

继续阅读