提示
redis
和tomcat
安装的过程就不发了,网上有很多,不会的去搜一下
<!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>
<!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>
sso-mvt
、sso-music
、分别将两个html页面分别放入两个文件夹,然后将两个文件夹放入tomcat
根目录中webapps
中,运行tomcathttp://localhost:8080/sso-mvt/index.html
SwitchHosts
,域名绑定如下http://www.sso:8080/sso-mvt/index.html
、http://www.sso.com:8080/sso-music/index.html
<?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>
#########################################################################
#
# 配置数据源信息
#
#########################################################################
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
#########################################################################
#
# 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
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);
}
}
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);
}
}
<!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>