CAS single sign-on implementation

1 Resource files

Resource download address : https://download.csdn.net/download/qq_15769939/15434524

Resources file website
Static website resources sso-mtc www.mtc.com
Static website resources sso-music www.music.com
Single sign-on source code sso-demo www.sso.com

1.1 Deploy a static website

Will sso-mtc, sso-musicto deploy to the Tomcat, tomcat port I use here is 8089, if you change it, change the need to synchronize static website source inside the port.

1.2 Integrate the background

The background source sso-demoopen in the IDEA, the port number springboot project here is 8090, if you change it, change the need to synchronize static website source inside the port.

1.3 Modify hosts

SwitchHosts New CAS plan

SwitchHosts download address : https://download.csdn.net/download/qq_15769939/15431964

# CAS

127.0.0.1 www.music.com

127.0.0.1 www.mtv.com

127.0.0.1 www.sso.com

右键 Switch to当前hosts

2 Timing diagram

Insert picture description here

UML source code download address :

https://download.csdn.net/download/qq_15769939/15433179

3 business realization

3.1 Login verification

   @GetMapping("/login")
    public String login(String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response){
    
    
        model.addAttribute("returnUrl",returnUrl);

        // 1. 获取userTicket门票,如果cookie中能够获取到,证明用户登陆过,此时签发一个临时票据并且回跳
        String userTicket = getCookie(request, COOKIE_USER_TICKET);
        boolean isVerified = verifyUserTicket(userTicket);
        if(isVerified) {
    
    
            String tmpTicket = createTmpTicket();
            return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
        }
        // 2. 用户从未登陆过,第一次进入则跳转到CAS的统一登录页面
        return "login";
    }

/**
     * 校验CAS全局门户门票
     * @param userTicket 全局门票
     * @return 结果
     */
    private boolean verifyUserTicket(String userTicket) {
    
    
        // 0. 验证CAS门票不能为空
        if(StringUtils.isBlank(userTicket)) {
    
    
            return false;
        }

        // 1. 验证CAS门票是否有效
        String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
        if(StringUtils.isBlank(userId)) {
    
    
            return false;
        }

        // 2. 验证门票对应的user会话是否存在
        String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
        if(StringUtils.isBlank(userRedis)) {
    
    
            return false;
        }
        return true;
    }

3.2 Single sign-on

The interface of CAS unified login needs to achieve 3 purposes:

  • 1 Login to create a user's global session (uniqueToken)
  • 2 Create a user global ticket (userTicket)
  • 3 Create a user temporary ticket for callback and return (tmpTicket)
 /**
     * CAS的统一登录接口
     *      目的:
     *          1. 登录创建用户的全局会话                        -> uniqueToken
     *          2. 创建用户全局门票,用以表示在CAS端是否登录        -> userTicket
     *          3. 创建用户临时票据,用于回跳回传                 -> tmpTicket
     * @param username 用户名
     * @param password 密码
     * @param returnUrl 返回地址
     * @param model view
     * @param request 请求
     * @param response 响应
     * @return 结果
     * @throws Exception 异常
     */
    @PostMapping("/doLogin")
    public String doLogin(String username,String password, String returnUrl, Model model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    

        model.addAttribute("returnUrl",returnUrl);
        // 0. 判断用户名和密码必须不为空
        if(StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
    
    
            model.addAttribute("errmsg","用户名或者密码不能为空");
            return "login";
        }

        // 1. 实现登录
        Users userResult = userService.queryUserForLogin(username, MD5Utils.getMD5Str(password));
        if(userResult == null) {
    
    
            model.addAttribute("errmsg","用户名或者密码不正确");
            return "login";
        }

//        userResult = setNullProperty(userResult);

        // 2.实现用户的redis会话
        String uniqueToken = UUID.randomUUID().toString().trim();
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(userResult, usersVO);
        usersVO.setUserUniqueToken(uniqueToken);
        redisOperator.set(REDIS_USER_TOKEN + ":" + userResult.getId(), JsonUtils.objectToJson(usersVO));

        // 3. 生成ticket门票,全局门票,代表用户在CAS端登陆过
        String userTicket = UUID.randomUUID().toString().trim();

        // 3.1 用户全局门票需要放入CAS端的cookie中
        setCookie(COOKIE_USER_TICKET, userTicket, response);

        // 4. userTicket关联用户ID,并且放入到redis中,代表这个用户有门票了,可以进入指定区域
        redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, userResult.getId());

        // 5. 生成临时票据,回跳到调用端网站,是由CAS端所签发的一个一次性的临时ticket
        String tmpTicket = createTmpTicket();

        /*
         * userTicket: 用于表示用户在CAS端的一个登录状态:已经登录
         * tmpTicket: 用于颁发给用户进行一次性的验证的票据,有时效性
         */

        /*
         * 举例:
         *      我们去动物园玩耍,大门口买了一张统一的门票,这个就是CAS系统的全局门票和用户全局会话。
         *      动物园里有一些小的景点,需要凭你的门票去领取一次性的票据,有了这张票据以后就能去一些小的景点游玩了。
         *      这样的一个个的小景点其实就是我们这里所对应的一个个的站点。
         *      当我们使用完毕这张临时票据以后,就需要销毁。
         */

//        return "login";
        return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
    }

    /**
     * 创建临时票据
     * @return 结果
     */
    private String createTmpTicket(){
    
    
        String tmpTicket = UUID.randomUUID().toString().trim();
        try {
    
    
            redisOperator.set(REDIS_TMP_TICKET + ":" + tmpTicket, MD5Utils.getMD5Str(tmpTicket),600);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return tmpTicket;
    }

    /**
     * 设置cookie
     * @param key key
     * @param val value
     * @param response 响应
     */
    private void setCookie(String key, String val, HttpServletResponse response) {
    
    
        Cookie cookie = new Cookie(key, val);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        response.addCookie(cookie);
    }

3.3 Verify temporary bill

 /**
     * 校验临时票据
     * @param tmpTicket 临时票据
     * @param request 请求
     * @param response 响应
     * @return 结果
     */
//    @PostMapping("/verifyTmpTicket")
    @RequestMapping("/verifyTmpTicket")
    @ResponseBody
    public R verifyTmpTicket(String tmpTicket,HttpServletRequest request,HttpServletResponse response) throws Exception {
    
    

        String userTicket2 = getCookie(request, COOKIE_USER_TICKET);
        System.out.println("  2222 " + userTicket2);

        // 使用一次性临时票据来验证用户是否已经登录,如果登陆过,把用户会话信息返回给站点
        // 使用完毕后,需要销毁临时票据
        String tmpTicketValue = redisOperator.get(REDIS_TMP_TICKET + ":" + tmpTicket);
        if(StringUtils.isBlank(tmpTicketValue)) {
    
    
            return R.errorUserTicket("用户票据异常");
        }

        // 0. 如果临时票据OK,则需要销毁,并且拿到CAS端cookie中的全局userTicket,以此再获取用户会话
        if(!tmpTicketValue.equals(MD5Utils.getMD5Str(tmpTicket))) {
    
    
            return R.errorUserTicket("用户票据异常");
        }else {
    
    
            // 销毁临时票据
            redisOperator.del(REDIS_TMP_TICKET + ":" + tmpTicket);
        }

        // 1. 验证并且获取用户的userTicket
        String userTicket = getCookie(request, COOKIE_USER_TICKET);
        String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
        if(StringUtils.isBlank(userId)) {
    
    
            return R.errorUserTicket("用户票据异常");
        }

        // 2. 验证门票对应的user会话是否存在
        String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
        if(StringUtils.isBlank(userRedis)) {
    
    
            return R.errorUserTicket("用户票据异常");
        }

        // 验证成功,返回OK,携带用户会话
        return R.ok(JsonUtils.jsonToPojo(userRedis, UsersVO.class));
    }

3.4 Logout

  /**
     * 退出登录
     * @param userId 用户ID
     * @param request 请求
     * @param response 响应
     * @return 结果
     */
    @PostMapping("/logout")
    @ResponseBody
    public R logout(String userId,HttpServletRequest request,HttpServletResponse response) throws Exception {
    
    

        // 0. 获取CAS中的用户门票
        String userTicket = getCookie(request, COOKIE_USER_TICKET);

        // 1. 清除userTicket票据 redis/cookie
        deleteCookie(COOKIE_USER_TICKET,response);
        redisOperator.del(REDIS_USER_TICKET + ":" + userTicket);

        // 2. 清除用户全局会话(分布式会话)
        redisOperator.del(REDIS_USER_TOKEN + ":" + userId);
        return R.ok();
    }

    /**
     * 获取Cookie
     * @param request 请求
     * @param key key
     * @return 结果
     */
    private String getCookie(HttpServletRequest request,String key) {
    
    
        Cookie[] cookieList = request.getCookies();

        if (cookieList == null || StringUtils.isBlank(key)) {
    
    
            return null;
        }

        for (Cookie cookie : cookieList) {
    
    
            System.out.println(cookie.getName() + ":" + cookie.getValue());
        }

        String cookieValue = null;
        for (int i = 0 ; i < cookieList.length; i ++) {
    
    
            if (cookieList[i].getName().equals(key)) {
    
    
                cookieValue = cookieList[i].getValue();
                break;
            }
        }

        return cookieValue;
    }

   /**
     * 删除Cookie
     */
    public void deleteCookie(String key,HttpServletResponse response) {
    
    
        Cookie cookie = new Cookie(key, null);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        cookie.setMaxAge(-1);
        response.addCookie(cookie);
    }

4 Related information

  • The blog post is not easy, everyone who has worked so hard to pay attention and praise, thank you

Guess you like

Origin blog.csdn.net/qq_15769939/article/details/114160863