分布式会话
@GetMapping("/setSession") public Object setSession(HttpServletRequest request) { HttpSession session = request.getSession(); session.setAttribute("userInfo", "new user"); session.setMaxInactiveInterval(3600); session.getAttribute("userInfo"); // session.removeAttribute("userInfo"); return "ok"; }

四: 代码实现通过redis进行分布式会话
1):用户注册,登录,修改用户信息的时候 生成一个token 放入缓存中作为分布式缓存token 同时将用户信息以及token放入cookie中
// 实现用户的redis会话 这里token直接简单生成 正常用该通过username+password+时间戳 String uniqueToken = UUID.randomUUID().toString().trim(); redisUtils.set(REDIS_USER_TOKEN + ":" + user.getId(), uniqueToken); UsersVO usersVO = new UsersVO(); BeanUtils.copyProperties(user, usersVO); usersVO.setUserUniqueToken(uniqueToken); // 将用户信息存储到cookie中 CookieUtils.setCookie(request, response, "user", JsonUtils.objectToJson(usersVO), true);
2):用户退出登录删除redis并清楚cookie
// 清除用户的相关信息的cookie CookieUtils.deleteCookie(request, response, "user"); // 用户退出登录,清除redis中user的会话信息 redisOperator.del(REDIS_USER_TOKEN + ":" + userId); /** * * @Description: 删除Cookie带cookie域名 * @param request * @param response * @param cookieName */ public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) { doSetCookie(request, response, cookieName, null, -1, false); // doSetCookie(request, response, cookieName, "", -1, false); } /** * * @Description: 设置Cookie的值,并使其在指定时间内生效 * @param request * @param response * @param cookieName * @param cookieValue * @param cookieMaxage cookie生效的最大秒数 * @param isEncode */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { try { if (cookieValue == null) { cookieValue = ""; } else if (isEncode) { cookieValue = URLEncoder.encode(cookieValue, "utf-8"); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage); if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); logger.info("========== domainName: {} ==========", domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } }
3):添加请求拦截器 校验是否是合法有效的token访问
package com.imooc.controller.interceptor; import com.imooc.utils.IMOOCJSONResult; import com.imooc.utils.JsonUtils; import com.imooc.utils.RedisOperator; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; public class UserTokenInterceptor implements HandlerInterceptor { @Autowired private RedisOperator redisOperator; public static final String REDIS_USER_TOKEN = "redis_user_token"; /** * 拦截请求,在访问controller调用之前 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // System.out.println("进入到拦截器,被拦截。。。"); String userId = request.getHeader("headerUserId"); String userToken = request.getHeader("headerUserToken"); if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) { String uniqueToken = redisOperator.get(REDIS_USER_TOKEN + ":" + userId); if (StringUtils.isBlank(uniqueToken)) { // System.out.println("请登录..."); returnErrorResponse(response, IMOOCJSONResult.errorMsg("请登录...")); return false; } else { if (!uniqueToken.equals(userToken)) { // System.out.println("账号在异地登录..."); returnErrorResponse(response, IMOOCJSONResult.errorMsg("账号在异地登录...")); return false; } } } else { // System.out.println("请登录..."); returnErrorResponse(response, IMOOCJSONResult.errorMsg("请登录...")); return false; } /** * false: 请求被拦截,被驳回,验证出现问题 * true: 请求在经过验证校验以后,是OK的,是可以放行的 */ return true; } public void returnErrorResponse(HttpServletResponse response, IMOOCJSONResult result) { OutputStream out = null; try { response.setCharacterEncoding("utf-8"); response.setContentType("text/json"); out = response.getOutputStream(); out.write(JsonUtils.objectToJson(result).getBytes("utf-8")); out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * 请求访问controller之后,渲染视图之前 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * 请求访问controller之后,渲染视图之后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
五: 如何实现相同顶级域名的单点登录 SSO

六: 通过CAS实现在不同的域名下单点登录

/**
* userTicket: 用于表示用户在CAS端的一个登录状态:已经登录
* tmpTicket: 用于颁发给用户进行一次性的验证的票据,有时效性
*/
/**
* 举例:
* 我们去动物园玩耍,大门口买了一张统一的门票,这个就是CAS系统的全局门票和用户全局会话。
* 动物园里有一些小的景点,需要凭你的门票去领取一次性的票据,有了这张票据以后就能去一些小的景点游玩了。
* 这样的一个个的小景点其实就是我们这里所对应的一个个的站点。
* 当我们使用完毕这张临时票据以后,就需要销毁。
*/
实现思路:
1: 用户第一次访问,进入/login接口,通过cookie获取全局票据没,没有,跳转统一登录界面
2: 实现登录
输入用户名密码 提交,进入/dologin接口,实现登录 获取用户信息,userId作为key userinfo作为value存储到redis中看/dologin接口实现 生成分布式会话 全部票据 临时票据 全局票据放入cookie中,跳转到verifyTmpTicket接口继续验证
3: 验证
进入verifyTmpTicket接口验证,通过临时票据换取cookie中全局票据,通过全局票据缓存用户信息
4: 用户在此访问另一个系统,系统调用登录接口 ,判断cookie中是否存在全局票据,存在全局票据,那么生成一个临时票据返回前端,前端按照之前的逻辑跳转到验证接口换取了用户信息
这样就实现了不同系统下的单点登录
临时票据的作用: (个人理解) 其实整个逻辑下来完全可以摘除临时票据的,那么为什么会有临时票据呢,个人理解还是为了安全性,因为临时票据是有实效的,当时效过了那么只能在此访问
/login登录接口,从cookie中获取全局票据在此获取临时票据,那么我们可以在这里加上一些黑名单等一些验证,防止一些黑客攻击网站,如果不用临时票据的话,那么只要cookie存在的时候,
黑客可以在这些系统下频繁的访问,切没有超时时间
代码: 后端
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 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(request, COOKIE_USER_TICKET); boolean isVerified = verifyUserTicket(userTicket); if (isVerified) { String tmpTicket = createTmpTicket(); return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket; } // 2. 用户从未登录过,第一次进入则跳转到CAS的统一登录页面 return "login"; } /** * 校验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 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: 用于颁发给用户进行一次性的验证的票据,有时效性 */ /** * 举例: * 我们去动物园玩耍,大门口买了一张统一的门票,这个就是CAS系统的全局门票和用户全局会话。 * 动物园里有一些小的景点,需要凭你的门票去领取一次性的票据,有了这张票据以后就能去一些小的景点游玩了。 * 这样的一个个的小景点其实就是我们这里所对应的一个个的站点。 * 当我们使用完毕这张临时票据以后,就需要销毁。 */ // return "login"; return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket; } @PostMapping("/verifyTmpTicket") @ResponseBody 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(request, COOKIE_USER_TICKET); 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("用户票据异常"); } // 验证成功,返回OK,携带用户会话 return IMOOCJSONResult.ok(JsonUtils.jsonToPojo(userRedis, UsersVO.class)); } @PostMapping("/logout") @ResponseBody public IMOOCJSONResult logout(String userId, HttpServletRequest request, HttpServletResponse response) throws Exception { // 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 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 val, HttpServletResponse response) { Cookie cookie = new Cookie(key, val); cookie.setDomain("sso.com"); cookie.setPath("/"); response.addCookie(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); } 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; } }
代码: 前端(简易版本,简单看看)
统一登录节界面:
<!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>
mtv-系统
<!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: { 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://www.sso.com: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://www.mtv.com: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://www.mtv.com: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://www.sso.com: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>
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"> var index = new Vue({ el: "#music", data: { userIsLogin: false, userInfo: {}, }, created() { var me = this; // 通过cookie判断用户是否登录 this.judgeUserLoginStatus(); // http://www.music.com:8080/sso-music/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验证获取用户会话 axios.defaults.withCredentials = true; axios.post('http://www.sso.com: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://www.music.com:8080/sso-music/index.html"; } else { alert(res.data.msg); console.log(res.data.msg); } }); } else { // 如果没有tmpTicket临时票据,说明用户从没登录过,那么就可以跳转至cas做统一登录认证了 window.location.href = app.SSOServerUrl + "/login?returnUrl=http://www.music.com:8080/sso-music/index.html"; } console.log(app.SSOServerUrl + "/login?returnUrl=" + window.location); } }, methods: { logout() { var userId = this.userInfo.id; axios.defaults.withCredentials = true; axios.post('http://www.sso.com: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>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现