CAS单点登录

一、时序图流程梳理

1.在hosts文件配置域名

www.mtv.com、www.music.com作为前端系统。www.sso.com、www.cas.com作为后台登录管理系统。

 二、构建CAS工程

 1.主要配置

(1)加依赖

<!--sso  cas 用spring构建登录页面-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

(2)在resource/templates下创建登录页面

 (3)yml文件配置

spring:
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    prefix: classpath:/templates/                  #前缀  页面所在的位置
    suffix: .html                                       #后缀

(4)controller层编写

a.测试单点登录系统

@Controller     //因为在这里会包含一个页面,所以不使用@RestController
public class SSOController {

    @GetMapping("/login")
    @ResponseBody    //@RestController不用加@ResponseBody  @Controller 需要加@ResponseBody,这样,会返回json字符串
    public Object login() {

        return "Hello World";
    }
}

测试:

b.测试  登录前端系统,跳转至sso系统的登录页面

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller     //因为在这里会包含一个页面,所以不使用@RestController
public class SSOController {

    @GetMapping("/login")
    //@ResponseBody    //@RestController不用加@ResponseBody  @Controller 需要加@ResponseBody,这样,会返回json字符串,测试时可以打开这个注解,然后方法里return "hello"
    public Object login(String returnUrl,
                        Model model,   //因为要把returnUrl传到templates/login.html页面,所以要加Model
                        HttpServletRequest request,
                        HttpServletResponse response) {

        model.addAttribute("returnUrl",returnUrl);

        //TODO 后续完善校验是否登录

        //用户从未登录过,第一次进入则跳转到CAS的统一登录页面
        return "login";    //返回templates/login.html页面
    }
}

访问前端系统: 

 跳转至登录页面:www.sso.com

三、用户登录与校验、创建用户会话

1.代码实现  在SSOController里,路径、参数根据login.html里配置的,见二.1.(2)

@PostMapping("/doLogin")
     public Object doLogin(String username,
                            String password,
                            String returnUrl,
                            Model model,   //因为要把returnUrl传到templates/login.html页面,所以要加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";
        }

        //实现用户的redis会话  如果没有集成redis,而是集成了其他中间件,比如memecache,则保存到memecache
        String uniqueToken = UUID.randomUUID().toString().trim();

        //设置用户会话
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(userResult,usersVO);  //拷贝的时候,UsersVO没有的属性。比如密码,邮箱,手机号没有,userResult就不会把这些信息拷贝过去了
        usersVO.setUserUniqueToken(uniqueToken);
        redisOperator.set(REDIS_USER_TOKEN + ":" +userResult.getId(),
                JsonUtils.objectToJson(usersVO));
        return "login";
    }

2.测试

(1) 访问www.mtv.com:8080/sso-mtv/ ,跳转至登录页面时,不输入用户名和密码

(2)访问www.mtv.com:8080/sso-mtv/ ,跳转至登录页面时,输入错误的用户名和密码

(3)访问www.mtv.com:8080/sso-mtv/ ,跳转至登录页面时, 输入正确的用户名和密码

(4)查看redis中保存的会话

四、 全局门票、临时票据

五、验证与销毁临时票据

六、换取用户会话

七、二次登录验证全局门票

八、实现注销

以上四到八的步骤代码实现如下,也就是一中整个流程图的实现。

package com.imooc.controller;

import com.imooc.pojo.Users;
import com.imooc.pojo.vo.UsersVO;
import com.imooc.service.UserService;
import com.imooc.utils.IMOOCJSONResult;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.MD5Utils;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Controller     //因为在这里会包含一个页面,所以不使用@RestController
public class SSOController {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisOperator redisOperator;

    public static final String REDIS_USER_TOKEN = "redis_user_token";
    public static final String REDIS_USER_TICKET = "redis_user_ticket";
    public static final String REDIS_TMP_TICKET = "redis_tmp_ticket";
    public static final String COOKIE_USER_TICKET = "cookie_user_ticket";

    @GetMapping("/login")
    //@ResponseBody    //@RestController不用加@ResponseBody  @Controller 需要加@ResponseBody,这样,会返回json字符串,测试时可以打开这个注解,然后方法里return "hello"
    public String login(String returnUrl,
                        Model model,   //因为要把returnUrl传到templates/login.html页面,所以要加Model
                        HttpServletRequest request,
                        HttpServletResponse response) {

        model.addAttribute("returnUrl",returnUrl);

        //1. 获取userTicket门票,如果cookie中能够获取到,证明用户登录过,此时签发一个一次性的临时门票
        String userTicket = getCookie(COOKIE_USER_TICKET,request);
        boolean isVerified = verifyUserTicket(userTicket);
        if(isVerified){
            String tmpTicket = createTmpTicket();
            return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
        }

        //2. 用户从未登录过,第一次进入则跳转到CAS的统一登录页面
        return "login";      //返回templates/login.html页面
    }

    /**
     * 检验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;
    }

    /**
     *CAS的统一登录接口
     *       目的:
     *            1.登录后创建用户的全局会话    ->  uniqueToken
     *            2.创建用户全局门票,用以表示在CAS端是否登录   -> userTicket
     *            3.创建用户的临时票据,用于回跳回传            -> tmpTicket
     */
    @PostMapping("/doLogin")
     public Object doLogin(String username,
                            String password,
                            String returnUrl,
                            Model model,   //因为要把returnUrl传到templates/login.html页面,所以要加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";
        }

        //2. 实现用户的redis会话  如果没有集成redis,而是集成了其他中间件,比如memecache,则保存到memecache
        String uniqueToken = UUID.randomUUID().toString().trim();

        //设置用户会话
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(userResult,usersVO);  //拷贝的时候,UsersVO没有的属性。比如密码,邮箱,手机号没有,userResult就不会把这些信息拷贝过去了
        usersVO.setUserUniqueToken(uniqueToken);
        redisOperator.set(REDIS_USER_TOKEN + ":" +userResult.getId(),
                JsonUtils.objectToJson(usersVO));

        //3. 生成ticket门票,全局门票,代表用户在CAS端登录过
        //trim()  去掉末尾和开始的空格
        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;  //回跳
    }

    @PostMapping("/verifyTmpTicket")
    @ResponseBody    //@RestController不用加@ResponseBody  @Controller 需要加@ResponseBody,这样,会返回json字符串
    public IMOOCJSONResult verifyTmpTicket(String tmpTicket,
                       HttpServletRequest request,
                        HttpServletResponse response) throws Exception{

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

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

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

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

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


    @PostMapping("/logout")
    @ResponseBody    //@RestController不用加@ResponseBody  @Controller 需要加@ResponseBody,这样,会返回json字符串
    public IMOOCJSONResult logout(String userId,
                                           HttpServletRequest request,
                                           HttpServletResponse response) throws Exception{

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

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

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

        return IMOOCJSONResult.ok();
    }

    /**
     * 创建临时票据
     * @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;
    }

    private void setCookie(String key,String value,HttpServletResponse response){
        Cookie cookie = new Cookie(key,value);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        response.addCookie(cookie);
    }

    private String getCookie(String key,HttpServletRequest request){
        Cookie[] cookieList = request.getCookies();
        if(cookieList == null || StringUtils.isBlank(key)){
            return null;
        }

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

    private void deleteCookie(String key,HttpServletResponse response){
        Cookie cookie = new Cookie(key,null);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        cookie.setMaxAge(-1);  //时间设置为-1
        response.addCookie(cookie);
    }
}

九、最终测试

访问www.mtv.com,首先跳转至www.sso.com站点,进行登录,登录成功后,跳转至原来的www.mtv.com站点

 在同一台服务器下,访问www.music.com,可直接访问,因为刚刚已在www.mtv.com登录过,全局门票验证通过。

 退出登录测试

退出登录针对的是某一个站点,也就是说www.music.com退出登录,www.mtv.com还是能访问。

退出登录,就会把全局门票的cookie清空

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@所谓伊人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值