SSO之CAS单点+Redis(实现不同顶级域名的单点登录)

SSO之CAS单点+Redis(实现不同顶级域名的单点登录)

  • CAS运行流程
    在这里插入图片描述
  • CAS系统访问过程
    在这里插入图片描述

一、环境准备

  • MTV系统、Music系统
  • CAS系统
  • Redis

提示
redistomcat安装的过程就不发了,网上有很多,不会的去搜一下

二、MVT系统、Music系统代码构建并运行

2.1、MVT系统代码

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=0">
	<title>MTV系统</title>
	<script src="https://cdn.jsdelivr.net/npm/vue@2.6.9/dist/vue.js"></script>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="mtv">
	<h1>MTV系统</h1>
	<div v-if="userIsLogin != true">
		欢迎陌生人,请<a>登录</a>!
	</div>
	<div v-if="userIsLogin == true">
		欢迎<span style="color: green;">{{userInfo.username}}</span>登录系统!
		<br/>
		<button @click="logout">点我退出登录</button>
	</div>
</div>
<script type="text/javascript " src="js/app.js"></script>
<script type="text/javascript">
	var index = new Vue({
		el: "#mtv",
		data: {
			cookieDomain: ".mtv.com",
			userIsLogin: false,
			userInfo: {},
		},
		created() {
			let me = this;
			// 通过cookie判断用户是否登录
			this.judgeUserLoginStatus();

			// 判断用户是否登录
			let userIsLogin = this.userIsLogin;
			if (!userIsLogin) {
				// 如果没有登录,判断一下是否存在tmpTicket临时票据
				let tmpTicket = this.getUrlParam("tmpTicket");

				if (tmpTicket !== null && tmpTicket !== "" && tmpTicket !== undefined) {
					// 如果有tmpTicket临时票据,就携带临时票据发起请求到cas验证获取用户会话
					axios.defaults.withCredentials = true;
					axios.post('http://www.sso.com:8090/sso/verifyTmpTicket?tmpTicket=' + tmpTicket)
							.then(res => {
								if (res.data.status === 200) {
									let userInfo = res.data.data;
									this.userInfo = userInfo;
									this.userIsLogin = true;
									this.setCookie("user",  JSON.stringify(userInfo));
									window.location.href = "http://www.mtv.com:8080/sso-mtv/index.html";
								} else {
									alert(res.data.msg);
								}
							});
				} else {
					// 如果没有tmpTicket临时票据,说明用户从没登录过,那么就可以跳转至cas做统一登录认证了
					window.location.href = "http://www.sso.com:8090/sso/login?returnUrl=http://www.mtv.com:8080/sso-mtv/index.html";
				}
			}
		},
		methods: {
			/**
			 * 用户退出
			 */
			logout() {
				let userId = this.userInfo.id;
				axios.defaults.withCredentials = true;
				axios.post('http://www.sso.com:8090/sso/logout?userId=' + userId)
						.then(res => {
							if (res.data.status === 200) {
								let userInfo = res.data.data;
								this.userInfo = {};
								this.userIsLogin = false;
								this.deleteCookie("user");
								
								alert("退出成功!");
							} else {
								alert(res.data.msg);
							}
						});
			},
			/**
			 * 通过cookie判断用户是否登录
			 */
			judgeUserLoginStatus() {
				let userCookie = this.getCookie("user");
				if (userCookie !== null && userCookie !== undefined && userCookie !== "") {
					let userInfoStr = decodeURIComponent(userCookie);
					if (userInfoStr !== null && userInfoStr !== undefined && userInfoStr !== "") {
						let userInfo = JSON.parse(userInfoStr);
						// 判断是否是一个对象
						if ( typeof(userInfo)  == "object" ) {
							this.userIsLogin = true;
							this.userInfo = userInfo;
						} else {
							this.userIsLogin = false;
							this.userInfo = {};
						}
					}
				} else {
					this.userIsLogin = false;
					this.userInfo = {};
				}
			},
			/**
			 * 获取get请求携带的参数
			 */
			getUrlParam(paramName) {
				let reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)");    // 构造一个含有目标参数的正则表达式对象
				let r = window.location.search.substr(1).match(reg);            // 匹配目标参数
				if (r != null) return decodeURI(r[2]); return null;             // 返回参数值
			},
			/**
			 * 获取cookie
			 */
			getCookie(cname) {
				let name = cname + "=";
				let ca = document.cookie.split(';');
				for (let i = 0; i < ca.length; i++) {
					let c = ca[i];
					while (c.charAt(0) === ' ') c = c.substring(1);
					if (c.indexOf(name) !== -1){
						return c.substring(name.length, c.length);
					}
				}
				return "";
			},
			/**
			 * 设置cookie
			 */
			setCookie(name, value) {
				let Days = 365;
				let exp = new Date();
				exp.setTime(exp.getTime() + Days*24*60*60*1000);
				let cookieContent = name + "="+ encodeURIComponent (value) + ";path=/;";
				if (this.cookieDomain !== null && this.cookieDomain !== undefined && this.cookieDomain !== '') {
					cookieContent += "domain=" + this.cookieDomain;
				}
				document.cookie = cookieContent + cookieContent;
			},
			/**
			 * 删除cookie
			 */
			deleteCookie(name) {
				let cookieContent = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
				if (this.cookieDomain !== null && this.cookieDomain !== undefined && this.cookieDomain !== '') {
					cookieContent += "domain=" + this.cookieDomain;
				}
				document.cookie = cookieContent;
			}
		},
	});
</script>
</body>
</html>

2.2、Music系统代码

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=0">
	<title>MUSIC系统</title>
	<script src="https://cdn.jsdelivr.net/npm/vue@2.6.9/dist/vue.js"></script>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
	<div id="music">
		<h1>MUSIC系统</h1>
		<div v-if="userIsLogin != true">
			欢迎陌生人,请<a>登录</a>!
		</div>
		<div v-if="userIsLogin == true">
			欢迎<span style="color: green;">{{userInfo.username}}</span>登录系统!
			<br/>
			<button @click="logout">点我退出登录</button>
		</div>
	</div>
	<script type="text/javascript " src="js/app.js"></script>
	<script type="text/javascript">
		let index = new Vue({
			el: "#music",
			data: {
				cookieDomain: ".music.com",
				userIsLogin: false,
				userInfo: {},
			},
			created() {
				let me = this;
				// 通过cookie判断用户是否登录
				this.judgeUserLoginStatus();

				// 判断用户是否登录
				let userIsLogin = this.userIsLogin;
				if (!userIsLogin) {
					// 如果没有登录,判断一下是否存在tmpTicket临时票据
					let tmpTicket = this.getUrlParam("tmpTicket");

					if (tmpTicket !== null && tmpTicket !== "" && tmpTicket !== undefined) {
						// 如果有tmpTicket临时票据,就携带临时票据发起请求到cas验证获取用户会话
						axios.defaults.withCredentials = true;
						axios.post('http://www.sso.com:8090/sso/verifyTmpTicket?tmpTicket=' + tmpTicket)
							.then(res => {
								if (res.data.status === 200) {
									let userInfo = res.data.data;
									this.userInfo = userInfo;
									this.userIsLogin = true;
									this.setCookie("user",  JSON.stringify(userInfo));

									window.location.href = "http://www.music.com:8080/sso-music/index.html";
								} else {
									alert(res.data.msg);
								}
							});
					} else {
						// 如果没有tmpTicket临时票据,说明用户从没登录过,那么就可以跳转至cas做统一登录认证了
						window.location.href = "http://www.sso.com:8090/sso/login?returnUrl=http://www.music.com:8080/sso-music/index.html";
					}
				}
			},
			methods: {
				/**
				 * 用户退出
				 */
				logout() {
					let userId = this.userInfo.id;
					axios.defaults.withCredentials = true;
					axios.post('http://www.sso.com:8090/sso/logout?userId=' + userId)
						.then(res => {
							if (res.data.status === 200) {
								let userInfo = res.data.data;
								this.userInfo = {};
								this.userIsLogin = false;
								this.deleteCookie("user");

								alert("退出成功!");
							} else {
								alert(res.data.msg);
							}
						});
				},
				/**
				 * 通过cookie判断用户是否登录
				 */
				judgeUserLoginStatus() {
					let userCookie = this.getCookie("user");
					if (userCookie !== null && userCookie !== undefined && userCookie !== "") {
						let userInfoStr = decodeURIComponent(userCookie);
						if (userInfoStr !== null && userInfoStr !== undefined && userInfoStr !== "") {
							let userInfo = JSON.parse(userInfoStr);
                            // 判断是否是一个对象
                            if ( typeof(userInfo)  == "object" ) {
                                this.userIsLogin = true;
                                this.userInfo = userInfo;
                            } else {
                                this.userIsLogin = false;
                                this.userInfo = {};
                            }
						}
					} else {
						this.userIsLogin = false;
						this.userInfo = {};
					}
				},
				/**
				 * 获取get请求携带的参数
				 */
				getUrlParam(paramName) {
					let reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)");    // 构造一个含有目标参数的正则表达式对象
					let r = window.location.search.substr(1).match(reg);            // 匹配目标参数
					if (r != null) return decodeURI(r[2]); return null;             // 返回参数值
				},
				/**
				 * 获取cookie
				 */
				getCookie(cname) {
					let name = cname + "=";
					let ca = document.cookie.split(';');
					for (let i = 0; i < ca.length; i++) {
						let c = ca[i];
						while (c.charAt(0) === ' ') c = c.substring(1);
						if (c.indexOf(name) !== -1){
							return c.substring(name.length, c.length);
						}
					}
					return "";
				},
				/**
				 * 设置cookie
				 */
				setCookie(name, value) {
					let Days = 365;
					let exp = new Date();
					exp.setTime(exp.getTime() + Days*24*60*60*1000);
					let cookieContent = name + "="+ encodeURIComponent (value) + ";path=/;";
					if (this.cookieDomain !== null && this.cookieDomain !== undefined && this.cookieDomain !== '') {
						cookieContent += "domain=" + this.cookieDomain;
					}
					document.cookie = cookieContent + cookieContent;
				},
				/**
				 * 删除cookie
				 */
				deleteCookie(name) {
					let cookieContent = name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
					if (this.cookieDomain !== null && this.cookieDomain !== undefined && this.cookieDomain !== '') {
						cookieContent += "domain=" + this.cookieDomain;
					}
					document.cookie = cookieContent;
				}
			}
		});
	</script>
</body>

</html>

2.3、将两个系统运行起来

  • 创建两个文件夹一个叫sso-mvtsso-music、分别将两个html页面分别放入两个文件夹,然后将两个文件夹放入tomcat根目录中webapps中,运行tomcat
  • 访问:http://localhost:8080/sso-mvt/index.html

2.4、域名绑定

  • 下载软件SwitchHosts,域名绑定如下
    在这里插入图片描述
  • 访问:http://www.sso:8080/sso-mvt/index.htmlhttp://www.sso.com:8080/sso-music/index.html

三、CAS系统构建

3.1、创建SpringBoot工程

  • 网上教程很多,不会的去搜一下

3.2、导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>eat_shop-dev</artifactId>
        <groupId>com.zzm</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eat_shop-dev-sso</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.zzm</groupId>
            <artifactId>eat_shop-dev-service</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

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

    </dependencies>

</project>

3.3、配置文件编写

3.3.1、application.yml

#########################################################################
#
# 配置数据源信息
#
#########################################################################
spring:
  profiles:
    active: dev
  datasource:                                     # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource      # 数据源类型:HikariCP
    driver-class-name: com.mysql.cj.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              #  连接的生命时长(毫秒),超时而且没被使用则被释放,默认:30分钟
      connection-test-query: SELECT 1
  servlet:
    multipart:
      max-file-size: 512000 # 文件上传大小限制为500kb = 500 * 1024
      max-request-size: 512000 # 请求大小限制为500kb
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    prefix: classpath:/templates/
    suffix: .html

#########################################################################
#
# mybatis 配置
#
#########################################################################
mybatis:
  type-aliases-package: com.zzm.pojo            # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件
  # configuration:
  #  log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


#########################################################################
#
# mybatis mapper配置
#
#########################################################################
mapper:
  mappers: com.zzm.my.mapper.MyMapper
  not-empty: false
  identity: MYSQL
#分页插件配置
pagehelper:
  helper-dialect: mysql
  support-methods-arguments: true
3.3.2、application-dev.yml
#########################################################################
#
# web访问端口号 约定:8888
#
#########################################################################
server:
  port: 8090
  tomcat:
    uri-encoding: UTF-8
  jetty:
    max-http-post-size: 80KB

#########################################################################
#
# 配置数据源信息、redis 配置
#
#########################################################################
spring:
  # 数据源配置
  datasource:                                     # 数据源的相关配置
    url: jdbc:mysql://127.0.0.1:3306/eat_shop?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8
    password: 你的密码
  # redis 配置
  redis:
    database: 1
    host: 你的ip
    port: 6379
    password: index密码


#########################################################################
#
# mybatis 配置
#
#########################################################################
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.4、实现跨域

  • 创建config文件夹,在config中新建CorsConfig.java,代码如下:
package com.zzm.sso.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    public CorsConfig() {
    }

    @Bean
    public CorsFilter corsFilter() {
        // 1. 添加cors配置信息
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:8090");
        config.addAllowedOrigin("http://www.sso.com");
        config.addAllowedOrigin("http://www.sso.com:8080");
        config.addAllowedOrigin("http://www.mtv.com");
        config.addAllowedOrigin("http://www.mtv.com:8080");
        config.addAllowedOrigin("http://www.music.com");
        config.addAllowedOrigin("http://www.music.com:8080");
        config.addAllowedOrigin("*");

        // 设置是否发送cookie信息
        config.setAllowCredentials(true);

        // 设置允许请求的方式
        config.addAllowedMethod("*");

        // 设置允许的header
        config.addAllowedHeader("*");

        // 2. 为url添加映射路径
        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/**", config);

        // 3. 返回重新定义好的corsSource
        return new CorsFilter(corsSource);
    }

}

3.5、CAS主要代码实现

  • 创建controller文件夹,在文件夹下创建SSOController.java,代码如下:
package com.zzm.sso.controller;

import com.zzm.pojo.Users;
import com.zzm.pojo.vo.UsersVO;
import com.zzm.service.UserService;
import com.zzm.utils.JSONResult;
import com.zzm.utils.JsonUtils;
import com.zzm.utils.MD5Utils;
import com.zzm.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
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.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
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
@RequestMapping("/sso")
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")
    public String login(String returnUrl,
                        Model 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;
        }


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

    /**
     * 校验CAS全局用户门票
     */
    private boolean verifyUserTicket(String userTicket){

        // 1.验证CAS门票不能为空
        if (StringUtils.isBlank(userTicket)){
            return false;
        }

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

        // 3.验证门票对应的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);


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

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

        // 3.实现用户的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));

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

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

        // 5.userTicket关联用户id,并且放入到redis中,代表这个用户有门票了,可以在子系统中游玩
        redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, usersVO.getId());

        // 6.生成临时票据,回跳调用端网站,是由CAS端签发的
        String tmpTicket = createTmpTicket();

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

        /**
         * 举例:
         *      动物园门票(userTicket):CAS系统的全局门票和用户全局绘画
         *      临时票据(tmpTicket):由动物园门票获取园内其他景区小的临时票据,用完就销毁
         */

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

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

        // 1.如果零时票据OK,则需要销毁,并且拿到CAS端COOKie中全局userTicket,一次再次获取用户临时票据
        try {
            if(!tmpTicketValue.equals(MD5Utils.getMD5Str(tmpTicket))){
                return JSONResult.errorMsg("用户票据异常");
            }else {
                // 销毁临时票据
                redisOperator.del(REDIS_TMP_TICKET + ":" + tmpTicket);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

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

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

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

    @PostMapping("/logout")
    @ResponseBody
    public JSONResult logout(String userId,
                             HttpServletRequest request,
                             HttpServletResponse response) {

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

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

        // 3.清除用户全局会话(分布式会话)
        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;
    }

    /**
     * 设置Cookie
     */
    private void setCookie(String key, String value, HttpServletResponse response){

        Cookie cookie = new Cookie(key, value);
        cookie.setDomain("sso.com");
        cookie.setPath("/");

        response.addCookie(cookie);
    }

    /**
     * 获取cookie
     */
    private String getCookie(String key, HttpServletRequest request){
        Cookie[] cookies = request.getCookies();

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

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

        return cookieValue;
    }

    /**
     * 删除cookie
     */
    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);
    }
}

3.6、登录页面实现

  • 废话不多说,直接上代码
<!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="/sso/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>

四、项目目录结构

  • 整个项目目录如下:
    在这里插入图片描述
  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
当用户第一次访问web应用系统1的时候,因为还没有录,会被引导到认证中心进行录;根据用户提供的录信息,认证系统进行身份效验,如果通过效验,返回给用户一个认证的凭据;用户再访问别的web应用的时候就会将这个Token带上,作为自己认证的凭据,应用系统接受到请求之后会把Token送到认证中心进行效验,检查Token的合法性。如果通过效验,用户就可以在不用再次录的情况下访问应用系统2和应用系统3了。所有应用系统共享一个身份认证系统。认证系统的主要功能是将用户的录信息和用户信息库相比较,对用户进行录认证;认证成功后,认证系统应该生成统一的认证标志,返还给用户。另外,认证系统还应该对Token进行效验,判断其有效性。 所有应用系统能够识别和提取Token信息要实现SSO的功能,让用户只录一次,就必须让应用系统能够识别已经录过的用户。应用系统应该能对Token进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否录过,从而完成单点登录的功能。 比如说,我现在有3个分站点和1个认证中心(总站)。当用户访问分站点的时候,分站点会发Token到验证中心进行验证。验证中心判断用户是否已经录。如果未录,则返回到验证中心录入口进行录,否之则返回Token验证到分站点,直接进入分站点
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值