一文速通单点登录

1 cookie

1.1 什么是cookie

Cookie 是存储在用户本地终端上的小型文本文件,主要用于识别用户身份和跟踪会话

详细来说,Cookie 是由网站服务器生成的,并通过用户的浏览器进行存储和管理。它们通常包含一些经过加密的数据,用于辨别用户以及保存用户的某些设置或信息。

1.2 cookie的作用

  • 会话状态管理:当用户登录一个网站时,服务器会设置一个包含会话ID的 Cookie,这样在用户浏览不同页面时,服务器能够通过这个会话ID识别用户的状态,保持用户的登录状态。例如,在线购物网站会使用 Cookie 来记录用户的登录状态,使得用户可以在不输入密码的情况下浏览和购买商品。

  • 个性化设置:网站允许用户根据自己的喜好进行个性化设置,如选择网站的主题颜色或布局。这些设置信息可以通过 Cookie 存储,当用户下次访问时,网站能够读取 Cookie 中的信息并应用这些设置,提供更加个性化的用户体验。

  • 浏览器行为跟踪:网站为了分析用户的访问习惯,了解用户的偏好,会通过 Cookie 跟踪用户的行为。例如,一个新闻网站可能会通过 Cookie 记录用户阅读的文章类型,从而向用户推荐更多类似的内容。

  • 自动填充信息:在一些需要频繁登录的网站,如邮箱服务,用户可以选择“记住我”或“自动登录”选项。这时,网站会在用户的浏览器中设置一个包含用户名和密码信息的 Cookie,以便在下次访问时自动填充这些信息,简化登录过程。

  • 购物车功能:电商平台会使用 Cookie 来跟踪用户的购物车内容。即使用户关闭了浏览器或离开网站,当他们再次回来时,购物车内的商品依然保留,这是通过读取存储在 Cookie 中的购物车信息实现的。

  • 游戏分数记录:在线游戏网站会使用 Cookie 来记录玩家的游戏分数或进度。这样,玩家即使在不同的设备上玩游戏,也能够同步他们的游戏数据,继续之前的游戏进度。

总的来说,Cookie 的作用是多方面的,它们通过存储和传递信息,帮助网站提供更加便捷、个性化的服务,同时也支持网站的数据分析和用户体验优化。然而,用户也应当注意管理自己的 Cookie,以保护个人隐私和数据安全。

1.3 cookie的属性

(1)Name/Value:设置Cookie的名称及相对应的值,对于认证Cookie,Value值包括Web服务器所提供的访问令牌 。

(2)Expires属性:设置Cookie的生存期。有两种存储类型的Cookie:会话性与持久性。Expires属性缺省时,为会话性Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效 。

(3)Path属性:定义了Web站点上可以访问该Cookie的目录 。

(4)Domain属性:指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 domain属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围 。

(5)Secure属性:指定是否使用https安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到ssl证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。由于许多用户缺乏安全意识,因而仍可能连接到Pharming攻击所伪造的网站 。

(6)HTTPOnly 属性 :用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头 。

2 session

2.1 什么是session

Session是一种记录客户端状态的机制,用于在服务器端保持用户的状态信息

Session的主要作用是在多个页面请求之间保持用户的状态,使得用户在进行一系列操作时,服务器能够识别并关联这些操作为同一用户的活动。这在需要用户登录的web应用中尤为重要,因为它允许服务器跟踪用户的身份和行为。

2.2 session的作用

Session的作用是记录用户在Web应用程序中的活动状态,以便服务器能够识别和跟踪用户。它允许服务器在多个页面请求之间保持用户的状态信息,使得用户在进行一系列操作时,服务器能够关联这些操作为同一用户的活动。

以下是一些实际例子来说明Session的作用:

  1. 购物车:当用户在电子商务网站上浏览商品并添加到购物车时,服务器可以使用Session来存储购物车中的商品列表。这样,即使用户在不同的页面之间跳转,购物车中的商品仍然可以保留,直到用户完成购买或退出登录。

  2. 用户认证:当用户登录到Web应用程序时,服务器可以使用Session来存储用户的登录状态和身份验证信息。这样,在用户进行后续操作时,服务器可以通过检查Session来确定用户是否已登录,并根据需要提供相应的访问权限。

  3. 个性化设置:Web应用程序可以使用Session来存储用户的个性化设置,例如语言偏好、主题颜色等。这样,在用户进行后续操作时,服务器可以根据Session中的信息来应用这些设置,为用户提供更加个性化的用户体验。

  4. 表单数据:当用户填写一个长表单时,服务器可以使用Session来暂存表单的数据。这样,如果用户在填写过程中意外关闭了浏览器或刷新了页面,服务器可以从Session中恢复之前填写的数据,避免用户重新输入。

总之,Session的作用是在Web应用程序中维护用户的状态信息,使得服务器能够识别和跟踪用户的行为,并提供更加个性化和连贯的用户体验。

2.3 session是如何运作的

Session的工作原理包括以下几个关键点:

  1. 创建:当用户首次访问服务器时,服务器会创建一个Session对象,该对象包含用于识别用户的会话ID。

  2. 存储:Session对象存储在服务器上,而不是客户端,这与Cookie不同,后者是存储在客户端的小型数据文件。

  3. 会话ID:服务器会将会话ID发送给客户端,通常通过Cookie来实现。每次客户端向服务器发送请求时,都会携带这个会话ID,以便服务器能够识别用户。

  4. 状态保持:用户在应用程序的不同Web页之间跳转时,存储在Session对象中的变量不会丢失,而是在整个用户会话中一直存在。

  5. 过期:当会话过期或被用户放弃(如关闭浏览器或注销)后,服务器将终止该会话。

当用户首次访问网站时,服务器会创建一个Session对象,这个对象包含用于识别用户的会话ID。Session数据存储在服务器上,而不是客户端,这样可以避免敏感信息的暴露。

为了在多个请求之间识别用户,服务器会在用户的浏览器中设置一个Cookie,这个Cookie包含会话ID。当用户再次访问网站时,浏览器会自动发送这个Cookie,服务器通过匹配Cookie中的会话ID来恢复用户的会话状态。

在实践中,这种结合使用的方式允许网站在不同的页面请求之间保持用户的信息,如登录状态、个性化设置等。例如,当用户登录一个网站时,服务器可能会在Session中存储用户的登录信息,同时在用户的浏览器中设置一个包含会话ID的Cookie。这样,当用户浏览网站的其他页面时,服务器可以通过Cookie中的会话ID来识别用户,并加载相应的Session数据,从而提供个性化的内容或服务。

2.4 session和cookie的区别

Session和Cookie的主要区别在于存储位置、安全性以及数据存储方式。具体如下:

存储位置

  • Cookie:存储在客户端,通常位于用户的浏览器中。

  • Session:存储在服务器端,不直接暴露给用户。

    安全性

  • Cookie:由于存储在客户端,容易受到跨站脚本攻击(XSS)等安全威胁。

  • Session:存储在服务器端,相对更加安全,不容易被非法访问。

    数据存储方式

  • Cookie:可以存储少量的用户偏好设置或其他信息。

  • Session:可以存储更多的用户会话数据,如登录状态、购物车信息等。

总的来说,Session和Cookie都是Web开发中重要的技术,它们各自有不同的特点和适用场景。了解它们的区别有助于开发者根据实际情况选择合适的技术来维护用户状态和提升网站的安全性。

3 单点登录

3.1 单什么是点登录

单点登录(Single Sign-On,简称SSO)是一种身份验证服务,允许用户使用一组凭据(如用户名和密码)访问多个应用程序或系统。当用户成功登录一个应用程序后,他们无需再次输入凭据即可访问其他已授权的应用程序。

举个例子,假设你在一个公司工作,这个公司有多个内部应用程序,如邮箱、日历、文档管理等。当你首次登录这些应用程序时,你需要分别输入用户名和密码。但是,如果公司启用了单点登录功能,那么在第一次登录成功后,你将无需再次输入用户名和密码即可访问其他应用程序。这意味着你只需要记住一个用户名和密码,就可以访问所有相关的应用程序。

单点登录的好处包括:

  1. 提高用户体验:用户无需记住多个用户名和密码,只需一次登录即可访问所有相关应用程序。

  2. 降低安全风险:用户只需保护一个凭证集,减少了因忘记密码而引发的安全问题。

  3. 简化管理:管理员只需维护一个用户目录,而不是为每个应用程序单独管理用户信息。

总之,单点登录是一种方便、安全且易于管理的认证方式,可以让用户轻松访问多个应用程序。

3.2 单点登录流程

单点登录(SSO)的流程涉及多个步骤,确保用户在登录一个系统后能够无缝访问其他所有系统而无需再次登录。以下是SSO的典型流程:

  1. 初始登录请求:用户首次尝试访问某个服务(如app1.a.com)时,该服务会识别出用户未登录状态,并引导用户到SSO认证服务器(sso.a.com)进行认证。

  2. SSO认证服务器验证:SSO认证服务器要求用户提供登录凭证(用户名和密码)。完成身份验证后,SSO认证服务器会在自己的服务端的session中记录用户的登录状态,并生成一个认证凭据,通常是一个包含会话信息的cookie或者token。

  3. 颁发凭据:一旦用户成功登录,SSO认证服务器会将会话信息加密成一个凭据(比如JSON Web Token, JWT),然后将这个凭据返回给用户。

  4. 服务间的信任关系:当用户尝试访问其他相关联的服务(如app2.a.com)时,这些服务会信任SSO认证服务器颁发的凭据。用户向这些服务提供之前获得的凭据来证明自己已经登录。

  5. 凭据验证与授权:关联的服务会验证用户提供的凭据是否有效。如果验证通过,服务会允许用户访问,无需用户再次输入用户名和密码。

  6. 单点注销:作为SSO的一部分,当用户在SSO认证服务器上注销时,所有关联的服务也会同步注销该用户的会话,确保用户在所有服务上的会话都被安全地结束。

3.3 请求过程

3.3.1 首次登录

1 用户在A系统请求登录 请求发送至SSO系统进行校验 发现用户没有进行登录 重定向至SSO系统登录页面

2 在SSO系统输入账户和密码 进行登录校验

3 如果校验成功 存储用户会话至redis eg: key:userId value:userInfo

4 会话创建成功后 创建用户全局ticket(可用UUID生成) 存放至cookie(key:'user_ticket' value:user_ticket) 存放至redis(key:user_ticket value:userId)

5 创建用户临时凭证 存放至redis(key:tem_ticket value:tem_ticket) 同时作为参数返回给A系统

6 A系统接收到tem_ticket 携带临时凭证请求SSO系统校验接口

7 SSO系统通过redis校验临时凭证的正确性 如果没有问题 删除redis的临时凭证 通过cookie获取用户的全局凭证 通过全局凭证获取用户编号 通过用户编号获取用户信息

8 SSO系统将用户信息响应给A系统 A系统登录成功

3.3.2 二次登录

1 用户登录B系统 请求至SSO服务器 通过cookie获取用户的全局凭证 全局凭证获取用户编号 用户编号获取用户信息 用户校验已登录

2 校验通过之后 生成临时凭证存放至redis(key:tem_ticket value:tem_ticket) 同时作为参数返回给B系统

3 B系统接收到tem_ticket 携带临时凭证请求SSO系统校验接口

4 SSO系统通过redis校验临时凭证的正确性 如果没有问题 删除redis的临时凭证 通过cookie获取用户的全局凭证 通过全局凭证获取用户编号 通过用户编号获取用户信息

5 SSO系统将用户信息响应给B系统 B系统登录成功

3.3.3 注销

1 通过cookie获取用户的全局凭证 删除该cookie 删除redis中的全局凭证和用户会话 推出登录成功

3.4 代码实现

A、B系统页面

<!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>A系统</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>A系统</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: {
				userIsLogin: false,
				userInfo: {},
			},
			created() {
				var me = this;
				// 通过cookie判断用户是否登录
				this.judgeUserLoginStatus();

				// http://www.mtv.com:8080/sso-mtv/index.html

				// 判断用户是否登录
				var userIsLogin = this.userIsLogin;
				if (!userIsLogin) {
					// 如果没有登录,判断一下是否存在tmpTicket临时票据
					var tmpTicket = app.getUrlParam("tmpTicket");
					console.log("tmpTicket: " + tmpTicket);
					if (tmpTicket != null && tmpTicket != "" && tmpTicket != undefined) {
						// 如果有tmpTicket临时票据,就携带临时票据发起请求到cas验证获取用户会话
						var serverUrl = app.serverUrl;
						axios.defaults.withCredentials = true;
						axios.post('http://localhost:8090/verifyTmpTicket?tmpTicket=' + tmpTicket)
							.then(res => {
								if (res.data.status == 200) {
									var userInfo = res.data.data;
									console.log(res.data.data);
									this.userInfo = userInfo;
									this.userIsLogin = true;
									app.setCookie("user",  JSON.stringify(userInfo));
									window.location.href = "http://localhost:8080/sso-mtv/index.html";
								} else {
									alert(res.data.msg);
									console.log(res.data.msg);
								}
							});
					} else {
						// 如果没有tmpTicket临时票据,说明用户从没登录过,那么就可以跳转至cas做统一登录认证了
						window.location.href = app.SSOServerUrl + "/login?returnUrl=http://localhost:8080/sso-mtv/index.html";
					}

					console.log(app.SSOServerUrl + "/login?returnUrl=" + window.location);
				}
			},
			methods: {
				logout() {
					var userId = this.userInfo.id;
					axios.defaults.withCredentials = true;
					axios.post('http://localhost:8090/logout?userId=' + userId)
						.then(res => {
							if (res.data.status == 200) {
								var userInfo = res.data.data;
								console.log(res.data.data);
								this.userInfo = {};
								this.userIsLogin = false;

								app.deleteCookie("user");

								alert("退出成功!");
								
							} else {
								alert(res.data.msg);
								console.log(res.data.msg);
							}
						});
				},
				// 通过cookie判断用户是否登录
				judgeUserLoginStatus() {
					var userCookie = app.getCookie("user");
					if (
						userCookie != null &&
						userCookie != undefined &&
						userCookie != ""
					) {
						var userInfoStr = decodeURIComponent(userCookie);
						// console.log(userInfo);
						if (
							userInfoStr != null &&
							userInfoStr != undefined &&
							userInfoStr != ""
						) {
							var userInfo = JSON.parse(userInfoStr);
                            // 判断是否是一个对象
                            if ( typeof(userInfo)  == "object" ) {
                                this.userIsLogin = true;
                                // console.log(userInfo);
                                this.userInfo = userInfo;
                            } else {
                                this.userIsLogin = false;
                                this.userInfo = {};
                            }
						}
					} else {
						this.userIsLogin = false;
						this.userInfo = {};
					}
				}
			}
		});
	</script>
</body>

</html>
<!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>B系统</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>B系统</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: {
				userIsLogin: false,
				userInfo: {},
			},
			created() {
				var me = this;
				// 通过cookie判断用户是否登录
				this.judgeUserLoginStatus();

				// http://www.mtv.com:8080/sso-mtv/index.html

				// 判断用户是否登录
				var userIsLogin = this.userIsLogin;
				if (!userIsLogin) {
					// 如果没有登录,判断一下是否存在tmpTicket临时票据
					var tmpTicket = app.getUrlParam("tmpTicket");
					console.log("tmpTicket: " + tmpTicket);
					if (tmpTicket != null && tmpTicket != "" && tmpTicket != undefined) {
						// 如果有tmpTicket临时票据,就携带临时票据发起请求到cas验证获取用户会话
						var serverUrl = app.serverUrl;
						axios.defaults.withCredentials = true;
						axios.post('http://localhost:8090/verifyTmpTicket?tmpTicket=' + tmpTicket)
							.then(res => {
								if (res.data.status == 200) {
									var userInfo = res.data.data;
									console.log(res.data.data);
									this.userInfo = userInfo;
									this.userIsLogin = true;
									app.setCookie("user",  JSON.stringify(userInfo));
									window.location.href = "http://localhost:8080/sso-mtv/index.html";
								} else {
									alert(res.data.msg);
									console.log(res.data.msg);
								}
							});
					} else {
						// 如果没有tmpTicket临时票据,说明用户从没登录过,那么就可以跳转至cas做统一登录认证了
						window.location.href = app.SSOServerUrl + "/login?returnUrl=http://localhost:8080/sso-mtv/index.html";
					}

					console.log(app.SSOServerUrl + "/login?returnUrl=" + window.location);
				}
			},
			methods: {
				logout() {
					var userId = this.userInfo.id;
					axios.defaults.withCredentials = true;
					axios.post('http://localhost:8090/logout?userId=' + userId)
						.then(res => {
							if (res.data.status == 200) {
								var userInfo = res.data.data;
								console.log(res.data.data);
								this.userInfo = {};
								this.userIsLogin = false;

								app.deleteCookie("user");

								alert("退出成功!");
								
							} else {
								alert(res.data.msg);
								console.log(res.data.msg);
							}
						});
				},
				// 通过cookie判断用户是否登录
				judgeUserLoginStatus() {
					var userCookie = app.getCookie("user");
					if (
						userCookie != null &&
						userCookie != undefined &&
						userCookie != ""
					) {
						var userInfoStr = decodeURIComponent(userCookie);
						// console.log(userInfo);
						if (
							userInfoStr != null &&
							userInfoStr != undefined &&
							userInfoStr != ""
						) {
							var userInfo = JSON.parse(userInfoStr);
                            // 判断是否是一个对象
                            if ( typeof(userInfo)  == "object" ) {
                                this.userIsLogin = true;
                                // console.log(userInfo);
                                this.userInfo = userInfo;
                            } else {
                                this.userIsLogin = false;
                                this.userInfo = {};
                            }
						}
					} else {
						this.userIsLogin = false;
						this.userInfo = {};
					}
				}
			}
		});
	</script>
</body>

</html>

SSO系统登录页面

<!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>

SSO系统后端接口

package com.hogan.controller;

import com.hogan.pojo.Users;
import com.hogan.pojo.vo.UsersVo;
import com.hogan.service.impl.UserService;
import com.hogan.util.*;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * SSOController
 *
 * @author hogan
 * @since 2024/2/21 12:06 PM
 */
@Controller
public class SSOController {

    @Resource
    private UserService userService;

    @Resource
    private RedisUtil redisUtil;

    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("/login")
    public String login(String returnUrl,
                        Model model,
                        HttpServletRequest request,
                        HttpServletResponse response) {
        model.addAttribute("returnUrl", returnUrl);
        //1 校验用户是否登陆
        String userCookie = getUserCookie(request);
        //1.1 校验用户凭证
        if (StringUtils.isNotBlank(userCookie)) {
            //1.2 校验用户编号
            String userId = redisUtil.get(REDIS_USER_TICKET + userCookie);
            if (StringUtils.isNotBlank(userId)) {
                //1.3 校验用户登陆会话
                String userInfo = redisUtil.get(REDIS_USER_TOKEN + userId);
                if (StringUtils.isNotBlank(userInfo)) {
                    String tmpTicket = createEmpTicket();
                    return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
                }
            }
        }
        return "login";
    }

    /**
     * 用户登陆
     * 1 创建用户的登陆会话至redis  uniqueToken
     * 2 创建用户的全局会话         userTicket
     * 3 创建用户的临时会话         tmpTicket
     */
    @RequestMapping("/doLogin")
    public String doLogin(String returnUrl,
                        String username,
                        String password,
                        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
        UsersVo usersVo = new UsersVo();
        BeanUtils.copyProperties(userResult, usersVo);
        String token = UUID.randomUUID().toString().trim();
        usersVo.setToken(token);
        redisUtil.set(REDIS_USER_TOKEN + usersVo.getId(), JsonUtils.objectToJson(usersVo));
        //4 生成全局ticket 代表用户登陆成功
        String userTicket = UUID.randomUUID().toString().trim();
        Cookie cookie = new Cookie(COOKIE_USER_TICKET, userTicket);
        cookie.setPath("/");
        response.addCookie(cookie);
        //5 为用户添加全局ticket
        redisUtil.set(REDIS_USER_TICKET + userTicket, usersVo.getId());
        //6 生成临时ticket
        String tmpTicket = createEmpTicket();
        /**
         * userTicket: 用于表示用户在CAS端的一个登录状态:已经登录
         * tmpTicket: 用于颁发给用户进行一次性的验证的票据,有时效性
         */

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

    private String createEmpTicket() {
        String tmpTicket = UUID.randomUUID().toString().trim();
        try {
            redisUtil.set(REDIS_TMP_TICKET + tmpTicket,
                    MD5Utils.getMD5Str(tmpTicket), 600);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return tmpTicket;
    }

    /**
     * 用户登陆校验
     */
    @PostMapping("/verifyTmpTicket")
    @ResponseBody
    public JSONResult verifyTmpTicket(String tmpTicket,
                                      HttpServletRequest request,
                                      HttpServletResponse response) throws Exception {
        //1 校验临时ticket是否存在
        String temRedisTicket = redisUtil.get(REDIS_TMP_TICKET + tmpTicket);
        if (StringUtils.isBlank(temRedisTicket)) {
            return JSONResult.errorMsg("用户临时凭证校验不通过");
        }
        //2 校验临时ticket是否正确
        if (temRedisTicket.equals(MD5Utils.getMD5Str(tmpTicket))) {
            //2.1 如果临时凭证校验通过 销毁凭证
            redisUtil.del(REDIS_TMP_TICKET + tmpTicket);
        } else {
            //2.2 临时票据校验不通过
            return JSONResult.errorMsg("用户临时凭证校验不通过");
        }
        //3 获取用户登陆的全局凭证
        String userTicket = getUserCookie(request);
        if (StringUtils.isBlank(userTicket)) {
            return JSONResult.errorMsg("用户全局凭证校验不通过");
        }
        //4 获取用户编号
        String userId = redisUtil.get(REDIS_USER_TICKET + userTicket);
        //5 获取用户信息
        String userInfo = redisUtil.get(REDIS_USER_TOKEN + userId);
        if (StringUtils.isBlank(userInfo)) {
            return JSONResult.errorMsg("用户会话获取失败");
        }
        return JSONResult.ok(JsonUtils.jsonToPojo(userInfo, UsersVo.class));
    }


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

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

        // 1. 清除userTicket票据,redis/cookie
        Cookie cookie = new Cookie(COOKIE_USER_TICKET, null);
        cookie.setPath("/");
        response.addCookie(cookie);
        redisUtil.del(REDIS_USER_TICKET + ":" + userTicket);

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

        return JSONResult.ok();
    }

    /**
     * 获取用户全局登陆的凭证
     */
    private static String getUserCookie(HttpServletRequest request) {
        String userTicket = null;
        try {
            List<String> userTicketList = Arrays.stream(request.getCookies()).filter(s -> COOKIE_USER_TICKET.equals(s.getName())).map(Cookie::getValue).collect(Collectors.toList());
            if (userTicketList.size() > 0) {
                userTicket = userTicketList.get(0);
            }
        } catch (Exception e) {
            return null;
        }
        return userTicket;
    }
}

  • 31
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值