CAS单点登录

news2024/11/20 11:51:20

1.相同顶级域名的单点登录SSO

相同顶级域名的单点登录:SSO:SINGLE SIGN ON
单点登录可以通过基于用户会话的共享;分为两种,第一种:相同顶级域名;
原理是分布式会话完成的;关键是顶级域名的cookie值是可以共享的

比如说现在有个一级域名为 www.xxx.com,是教育类网站,但是xxx网有其他的产品线,可以通过构建二级域名提供服务给用户访问,比如:music.xxx.com,shop.xxx.com,blog.xxx.com等等,分别为xx音乐,xx电商以及xx博客等,用户只需要在其中一个站点登录,那么其他站点也会随之而登录。

也就是说,用户自始至终只在某一个网站下登录后,那么他所产生的会话,就共享给了其他的网站,实现了单点网站登录后,同时间接登录了其他的网站,那么这个其实就是单点登录,他们的会话是共享的,都是同一个用户会话。

Cookie + Redis 实现 SSO
那么之前我们所实现的分布式会话后端是基于redis的,如此会话可以流窜在后端的任意系统,都能获取到缓存中的用户数据信息,前端通过使用cookie,可以保证在同域名的一级二级下获取,那么这样一来,cookie中的信息userid和token是可以在发送请求的时候携带上的,这样从前端请求后端后是可以获取拿到的,这样一来,其实用户在某一端登录注册以后,其实cookie和redis中都会带有用户信息,只要用户不退出,那么就能在任意一个站点实现登录了。

那么这个原理主要也是cookie和网站的依赖关系,顶级域名 www.xxx.com和*.xxx.com的cookie值是可以共享的,可以被携带至后端的,比如设置为 .xxx.com,.t.xxx.com,如此是OK的。
二级域名自己的独立cookie是不能共享的,不能被其他二级域名获取,比如:music.xxx.com的cookie是不能被blog.xxx.com共享,两者互不影响,要共享必须设置为.xxx.com

2.不同顶级域名的单点登录

如果顶级域名都不一样,咋办?比如 wwww.xxx.com要和www.yyy.com的会话实现共享,这个时候又该如何?!
这个时候的cookie由于顶级域名不同,就不能实现cookie跨域了,每个站点各自请求到服务端,cookie无法同步。比如,www.xxx.com下的用户发起请求后会有cookie,但是他又访问了www.yyy.com,由于cookie无法携带,所以要求二次登录。

那么遇到顶级域名不同却又要实现单点登录该如何实现呢?我们来参考下面一张图:

在这里插入图片描述
如上图所示,多个系统之间的登录会通过一个独立的登录系统去做验证,它就相当于是一个中介公司,整合了所有人,你要看房经过中介允许拿钥匙就行,实现了统一的登录。那么这个就称之为CAS系统,CAS全称为Central Authentication Service即中央认证服务,是一个单点登录的解决方案,可以用于不同顶级域名之间的单点登录。构建两个静态站点来测试使用即可。
在CAS中的具体的流程参考如下时序图:
在这里插入图片描述

3.代码实现:

SSO-MTV;SSO-MUSIC为两个不同顶级域名的子系统;用于测试用的;运行在tomcat的8080端口;
依赖:

<dependencies>
		<!--  自定义Service   -->
        <dependency>
            <groupId>com.nly</groupId>
            <artifactId>foodie-dev-service</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
</dependencies>

部分配置信息:


############################################################
#
# web访问端口号  约定:8088
#
############################################################
server:
  tomcat:
    uri-encoding: UTF-8
  max-http-header-size: 80KB

############################################################
#
# 配置数据源信息
#
############################################################
spring:
  profiles:
    active: dev
  datasource:                                           # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: com.mysql.jdbc.Driver          # mysql驱动
    username: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1

  servlet:
    multipart:
      max-file-size: 512000 #文件上传大小限制为500kb
      max-request-size: 512000 #请求大小限制为500kb
#  session:
#    store-type: redis
  thymeleaf:
    mode: HTML
    encoding: utf-8
    prefix: classpath:/templates/
    suffix: .html

############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.nly.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件
  
############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.nly.my.mapper.MyMapper
  not-empty: false #在进行数据库操作的时候,判断表达式username! = null,是否追加username!=''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
server:
  port: 8090

spring:
  datasource:                                           # 数据源的相关配置
    url: jdbc:mysql://localhost:3306/foodie-shop-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    password: xxx
  redis:
    #redis单机单实例
    database: 2
    host: 192.168.56.102
    port: 6379
    timeout: 5000
    password: xxx

启动类

@SpringBootApplication
//扫描mybatis通用的包
@MapperScan(basePackages = "com.nly.mapper")
//扫描所有包以及相关组件包
@ComponentScan(basePackages = {"com.nly","org.n3r.idworker"})

public class Application {

    public static void main(String[] args) {

        SpringApplication.run(Application.class,args);
    }
    //ApplicationListener
}

构建登录的模版页:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>SSO单点登录</title>
</head>
<body>
<h1>欢迎访问单点登录系统</h1>
<form action="doLogin" method="post">
    <input type="text" name="username" placeholder="请输入用户名"/>
    <input type="password" name="password" placeholder="请输入密码"/>
    <input type="hidden" name="returnUrl" th:value="${returnUrl}">
    <input type="submit" value="提交登录"/>
</form>
<span style="color:red" th:text="${errmsg}"></span>

</body>
</html>
@Controller
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";


   /*
    @RequestMapping("/hello")
    @ResponseBody
    public  Object hello(){
        return "hello,world";
    }*/

    @RequestMapping("/login")
    public  String login(String returnUrl,
                         Model model,
                         HttpServletRequest request,
                         HttpServletResponse response){

        model.addAttribute("returnUrl", returnUrl);
        //从cookie中获取userTicket门票,如果cookie中能够获取到,说明用户登录过,签发tmpTicket即可
        String userTicket = getCookie(request,COOKIE_USER_TICKET);

        boolean isVerified = verifyUserTicket(userTicket);

        if (isVerified) {
            String tmpTicket = createTmpTicket();
            return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
        }

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

    private boolean verifyUserTicket(String userTicket){
        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  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";
        }
       //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:用于颁发给用户进行一次性的验证的票据,有时效性
         */
//        return "login";

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

    @PostMapping("/verifyTmpTicket")
    @ResponseBody
    public  JSONResult verifyTmpTicket(String tmpTicket,
                         HttpServletRequest request,
                         HttpServletResponse response) throws Exception {

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

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

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

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

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

    @PostMapping("/logout")
    @ResponseBody
    public JSONResult logout(String userId,
                             HttpServletRequest request,
                             HttpServletResponse response){
        //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 JSONResult.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 val,
                           HttpServletResponse response){
        Cookie cookie = new Cookie(key,val);
        cookie.setDomain("sso.com");
        cookie.setPath("/");
        response.addCookie(cookie);
    }

    private String getCookie(HttpServletRequest request,String key){
        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);
        response.addCookie(cookie);
    }
}

那么对于SSO的整个处理流程来讲,其实我们实现起来并不是很难,主要是为的理解整个流程,因为在面试过程中有可能会被问到。如果有兴趣的同学,可以去参考一下Apereo的CAS系统,是非常牛的,地址如下:
https://github.com/apereo/cas
https://www.apereo.org/projects/cas
(备注:后续会将所有的代码传到github上,有兴趣的可以关注一下)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1978995.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7月小游戏畅销榜Top 100:MMO游戏数量增多,26款新入榜

易采游戏网8月4日消息&#xff1a;2024年7月的小游戏畅销榜Top100已经揭晓&#xff0c;给广大游戏玩家带来了不少惊喜和期待。与上个月相比&#xff0c;本月的榜单不仅新入榜游戏数量达到了26款&#xff0c;还显示了MMO&#xff08;大型多人在线&#xff09;游戏的强劲增长趋势…

deform,一个超强的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个超强的 Python 库 - deform。 Github地址&#xff1a;https://github.com/Pylons/deform 在 Web 开发中&#xff0c;表单处理是一个常见且重要的任务。deform 是一个用于生…

巧用casaos共享挂载自己的外接硬盘为局域网共享

最近入手了个魔改机顶盒,已经刷好了的armbian,虽然是原生的,但是我觉得挺强大的,内置了很多 常用的docker和应用,只需要armbian-software 安装就行,缺点就是emmc太小了。 买到之后第一时间装上了casaos和1panel,想把外接移动硬盘挂载到局域网,只需: 1、安装必要的sam…

Parallels Desktop19让你的Mac无缝运行Windows!

大家好&#xff0c;我是你们的科技小伙伴&#xff0c;今天我要给大家安利一款神奇的软件——Parallels Desktop 19虚拟机。这款产品真的是让我眼前一亮&#xff0c;用起来简直不能更爽&#xff01; 让我们来聊聊为什么我们需要一个虚拟机。 想象一下&#xff0c;你是一个Mac用…

牛顿插值法代替泰勒公式

引入 例题 近似函数&#xff1a; 通过这个近似函数可以看出&#xff0c;若要证的函数超过二阶可导&#xff0c;那么就不适合用牛顿插值法代替泰勒公式 因为&#xff0c;后面的操作非常复杂&#xff0c;不划算了… 总结 我们可以通过牛顿插值法生成一个逼近曲线的直线&#xf…

贷款申请被拒,是银行故意在找茬吗?

贷款申请被拒&#xff0c;很多时候真不是银行故意找茬&#xff0c;而是咱们自己的一些“小动作”不经意间就把路给堵窄了。今天&#xff0c;咱们就来聊聊那些可能让贷款之路变得坎坷的“坑”&#xff0c;帮你顺利绕开它们。 首先&#xff0c;得说说那个最让人头疼的——逾期还款…

“数字孪生+大模型“:打造设施农业全场景数字化运营新范式

设施农业是一个高度复杂和精细化管理的行业,涉及环境控制、作物生长、病虫害防治、灌溉施肥等诸多环节。传统的人工管理模式已经难以应对日益增长的市场需求和管理挑战。智慧农业的兴起为设施农业带来了新的机遇。将前沿信息技术与农业生产深度融合,实现农业生产的数字化、网络…

立项技术路线选择

本章主要是简单聊聊技术路线&#xff0c;额涉及unity和虚幻&#xff0c;目的主要是给自己看的&#xff0c;记录下日期&#xff1a;2024.8.4 在今天&#xff0c;除游戏以外的厂商基本上采用c#的混合技术方案 如果需要的设备对象多。效果不需要极为精细&#xff0c;至少unity是绝…

从根儿上学习spring 八 之run方法启动第四段(2)

图2 我们接着上一篇接着来看refresh方法&#xff0c;我们上一小节说完了invokeBeanFactoryPostProcessors(beanFactory)方法&#xff0c;这一节我们来看registerBeanPostProcessors(beanFactory)方法。 从方法名称定义我们就能看出这个方法主要是用来注册BeanPostProcesor的。…

欧拉图,欧拉通路,欧拉回路,Hierholzer算法详解

文章目录 零、哥尼斯堡七桥问题一、欧拉图1.1 相关概念1.2 判别法&#xff08;不做证明&#xff09;1.3 Hierholzer算法1.4 代码实现1.4.1 邻接表存图1.4.2 链式前向星存图 二、OJ练习2.1 模板12.2 模板22.3 重新安排行程2.4 合法重新排列数对2.5 破解保险箱2.6 骑马修栅栏2.7 …

WebVirtMgr管理多台物理机

这篇文章只是讲一讲管理多台物理机遇到的坑&#xff0c;记录一下。目前时间紧张&#xff0c;空余时间再补充细节。 WebVirtMgr管控单台物理机很多文章能搜到&#xff0c;写的也都挺好。 管理多台的具体步骤我没碰到过&#xff0c;只能按照报错去一步步解决。 第一个问题&…

2024睿抗国赛赛后总结

题目可以去pta教育超市找 写第一题还很清醒。&#xff08;耗时15分钟&#xff09; #include<bits/stdc.h> using namespace std; string s; int sum 0,len 0; int cnt 0;int check(char c){if(c > a && c < z){return 1;}else if(c < Z &&…

【每日刷题】Day92

【每日刷题】Day92 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 面试题 16.05. 阶乘尾数 - 力扣&#xff08;LeetCode&#xff09; 2. 取近似值_牛客题霸_牛客网 (n…

贝叶斯学习方法:几种方法介绍

目录 1 拉普拉斯还原-轻松贝叶斯深度学习2 具有归一化流的变分推理3 基于条件归一化流的多元概率时间序列预测 1 拉普拉斯还原-轻松贝叶斯深度学习 深度学习的贝叶斯公式已被证明具有令人信服的理论性质&#xff0c;并提供实用的功能优势&#xff0c;例如改进预测不确定性量化…

多路I/O复用之select、poll、epoll

一、多进程/多线程模型的不足 为每个请求分配一个进程或线程的方式会带来较大的资源开销。创建和切换进程/线程需要消耗系统资源&#xff0c;包括内存、CPU 时间等。例如&#xff0c;在一个大规模的服务器环境中&#xff0c;如果同时有数千个请求到来&#xff0c;为每个请求创建…

C/C++烟花代码

目录 系列推荐 写在前面 烟花代码 代码分析 运行结果 写在后面 系列推荐 序号目录直达链接1爱心代码https://want595.blog.csdn.net/article/details/1363606842李峋同款跳动的爱心https://want595.blog.csdn.net/article/details/1397222493满屏飘字代码https://want59…

YOLOv8网络轻量化改进之ShuffleNetV2主干

目录 一、理论模型 二、代码修改 一、理论模型 首先是shuffleNet网络的理论介绍部分 论文地址:1807.11164 (arxiv.org) 这里是shufflenetv2网络的主要模块的结构,在网络结构中,通过步长来选择这两种不同的模块。步长为1的时候,对应模块c,步长为2的时候对应模块d。 二、…

Java 文件操作和 IO

1. 认识文件 狭义上的文件&#xff08;file&#xff09;&#xff1a;针对硬盘这种持久化存储的 IO 设备&#xff0c;当我们想要进行数据保存时&#xff0c;往往不是保存成一个整体&#xff0c;而是独立成一个个的单位进行保存&#xff0c;这种独立的单位就被抽象成文件的概念 …

2024第五届华数杯数学建模竞赛C题思路+代码

目录 原题背景背景分析 问题一原题思路Step1:数据读取与处理Step2:计算最高评分&#xff08;Best Score, BS&#xff09;Step3:统计各城市的最高评分&#xff08;BS&#xff09;景点数量 程序读取数据数据预处理 问题二原题思路Step1: 定义评价指标Step2: 收集数据Step3: 标准化…

【linux深入剖析】初识线程---线程概念

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1. Linux线程概念什么是线…