> 技术文档 > 基于Spring Session + Redis + JWT的单点登录实现

基于Spring Session + Redis + JWT的单点登录实现


实现思路

  1. 用户访问受保护资源时,若未认证则重定向到认证中心
  2. 认证中心验证用户身份,生成JWT令牌并存储到Redis
  3. 认证中心重定向回原应用并携带令牌
  4. 应用验证JWT有效性并从Redis获取会话信息
  5. 用户在其他应用访问时,通过相同机制实现单点登录

代码实现

<!DOCTYPE html><html lang=\"zh-CN\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <title>SSO 单点登录演示</title> <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\"> <style> .system-card { transition: all 0.3s ease; cursor: pointer; } .system-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); } .logged-in { border-left: 4px solid #198754; } .not-logged-in { border-left: 4px solid #dc3545; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 5px; } .online { background-color: #198754; } .offline { background-color: #6c757d; } </style></head><body> <div class=\"container my-5\"> <div class=\"row mb-4\"> <div class=\"col text-center\"> <h1 class=\"display-4\">单点登录(SSO)演示系统</h1> <p class=\"lead\">基于 Spring Session + Redis + JWT 实现</p> <div class=\"mt-4\" id=\"user-info\" style=\"display: none;\">  <p>当前用户: <span id=\"username\" class=\"fw-bold\">未登录</span></p>  <button id=\"logout-btn\" class=\"btn btn-danger btn-sm\">退出登录</button> </div> </div> </div> <div class=\"row mb-4\"> <div class=\"col-md-8 mx-auto\"> <div class=\"card\">  <div class=\"card-header bg-primary text-white\"> <h5 class=\"mb-0\">登录认证中心</h5>  </div>  <div class=\"card-body\"> <div id=\"login-form\"> <div class=\"mb-3\"> <label for=\"inputUsername\" class=\"form-label\">用户名</label> <input type=\"text\" class=\"form-control\" id=\"inputUsername\" value=\"admin\"> </div> <div class=\"mb-3\"> <label for=\"inputPassword\" class=\"form-label\">密码</label> <input type=\"password\" class=\"form-control\" id=\"inputPassword\" value=\"password\"> </div> <button id=\"login-btn\" class=\"btn btn-primary\">登录</button> </div>  </div> </div> </div> </div> <div class=\"row\"> <h3 class=\"mb-4\">应用系统</h3> <div class=\"col-md-4 mb-4\"> <div class=\"card system-card not-logged-in\">  <div class=\"card-body\"> <h5 class=\"card-title\">人力资源系统</h5> <p class=\"card-text\">访问员工信息、薪资数据等</p> <div class=\"d-flex justify-content-between align-items-center\"> <span><span class=\"status-indicator online\"></span>运行中</span> <span class=\"badge bg-secondary\" id=\"hr-status\">未登录</span> </div>  </div> </div> </div> <div class=\"col-md-4 mb-4\"> <div class=\"card system-card not-logged-in\">  <div class=\"card-body\"> <h5 class=\"card-title\">客户关系管理</h5> <p class=\"card-text\">管理客户信息和销售渠道</p> <div class=\"d-flex justify-content-between align-items-center\"> <span><span class=\"status-indicator online\"></span>运行中</span> <span class=\"badge bg-secondary\" id=\"crm-status\">未登录</span> </div>  </div> </div> </div> <div class=\"col-md-4 mb-4\"> <div class=\"card system-card not-logged-in\">  <div class=\"card-body\"> <h5 class=\"card-title\">财务系统</h5> <p class=\"card-text\">处理财务数据和报表</p> <div class=\"d-flex justify-content-between align-items-center\"> <span><span class=\"status-indicator online\"></span>运行中</span> <span class=\"badge bg-secondary\" id=\"finance-status\">未登录</span> </div>  </div> </div> </div> </div> <div class=\"row mt-5\"> <div class=\"col\"> <div class=\"card\">  <div class=\"card-header bg-info text-white\"> <h5 class=\"mb-0\">SSO 流程说明</h5>  </div>  <div class=\"card-body\"> <ol> <li>用户在任意系统访问受保护资源</li> <li>系统检查本地会话,若无有效会话则重定向到认证中心</li> <li>认证中心检查全局会话,若已登录则直接颁发令牌</li> <li>若未登录,显示登录页面,用户提交凭证</li> <li>认证中心验证凭证,创建全局会话,生成JWT令牌</li> <li>认证中心重定向回原系统并携带令牌</li> <li>原系统验证JWT有效性,创建本地会话</li> <li>用户访问其他系统时,重复上述流程实现单点登录</li> </ol>  </div> </div> </div> </div> </div> <script> document.addEventListener(\'DOMContentLoaded\', function() { const loginBtn = document.getElementById(\'login-btn\'); const logoutBtn = document.getElementById(\'logout-btn\'); const userInfoDiv = document.getElementById(\'user-info\'); const systems = [ { id: \'hr-status\', name: \'人力资源系统\' }, { id: \'crm-status\', name: \'客户关系管理\' }, { id: \'finance-status\', name: \'财务系统\' } ]; // 模拟登录功能 loginBtn.addEventListener(\'click\', function() { const username = document.getElementById(\'inputUsername\').value; const password = document.getElementById(\'inputPassword\').value; // 模拟认证过程 if (username && password) {  // 显示用户信息  document.getElementById(\'username\').textContent = username;  userInfoDiv.style.display = \'block\';  // 更新系统状态  systems.forEach(system => { const element = document.getElementById(system.id); element.className = \'badge bg-success\'; element.textContent = \'已登录\'; // 更新卡片样式 const card = element.closest(\'.system-card\'); card.classList.remove(\'not-logged-in\'); card.classList.add(\'logged-in\');  });  // 模拟JWT令牌生成和存储  const mockJwt = generateMockJWT(username);  localStorage.setItem(\'sso_token\', mockJwt);  alert(\'登录成功!已生成JWT令牌并存储在Redis中。\'); } else {  alert(\'请输入用户名和密码\'); } }); // 模拟退出功能 logoutBtn.addEventListener(\'click\', function() { // 清除用户信息 userInfoDiv.style.display = \'none\'; // 更新系统状态 systems.forEach(system => {  const element = document.getElementById(system.id);  element.className = \'badge bg-secondary\';  element.textContent = \'未登录\';  // 更新卡片样式  const card = element.closest(\'.system-card\');  card.classList.remove(\'logged-in\');  card.classList.add(\'not-logged-in\'); }); // 清除本地存储的令牌 localStorage.removeItem(\'sso_token\'); alert(\'已退出登录,全局会话已清除。\'); }); // 模拟点击系统卡片 document.querySelectorAll(\'.system-card\').forEach(card => { card.addEventListener(\'click\', function() {  const statusBadge = this.querySelector(\'.badge\');  if (statusBadge.textContent === \'未登录\') { // 检查是否有全局会话 const token = localStorage.getItem(\'sso_token\'); if (token) { // 有全局会话,直接登录该系统 statusBadge.className = \'badge bg-success\'; statusBadge.textContent = \'已登录\'; this.classList.remove(\'not-logged-in\'); this.classList.add(\'logged-in\'); alert(\'单点登录成功!使用Redis中的会话信息自动登录。\'); } else { // 无全局会话,跳转到认证中心 alert(\'未登录,跳转到认证中心...\'); // 实际场景中这里会重定向到认证中心 }  } else { alert(\'访问 \' + this.querySelector(\'.card-title\').textContent);  } }); }); // 模拟生成JWT令牌的函数 function generateMockJWT(username) { // 实际JWT包含header.payload.signature三部分 // 这里仅做演示,生成一个模拟的令牌 const header = btoa(JSON.stringify({ alg: \'HS256\', typ: \'JWT\' })); const payload = btoa(JSON.stringify({  sub: username,  iat: Date.now(),  exp: Date.now() + 3600000 // 1小时后过期 })); const signature = \'mock-signature\'; // 实际场景中会用密钥生成签名 return `${header}.${payload}.${signature}`; } }); </script></body></html>

技术实现说明

1. 核心组件

  • Spring Session: 用于统一会话管理,将HTTP会话存储到Redis中
  • Redis: 作为集中式会话存储,支持多个应用共享会话数据
  • JWT: 作为身份验证令牌,在应用间安全传递用户身份信息

2. 关键流程

  1. 用户访问应用系统:系统检查本地会话是否存在
  2. 未认证重定向:若无有效会话,重定向到认证中心并携带回调地址
  3. 认证中心检查:认证中心检查是否存在全局会话
  4. 用户登录:若无全局会话,用户提交凭证进行认证
  5. 创建会话:认证成功后,创建全局会话并存储到Redis
  6. 颁发令牌:生成JWT并重定向回原应用
  7. 验证令牌:原应用验证JWT有效性,创建本地会话
  8. 单点访问:用户访问其他应用时,重复上述流程实现单点登录

3. 后端实现要点(伪代码)

// 认证中心控制器@Controllerpublic class AuthController { @PostMapping(\"/login\") public String login(String username, String password, String redirectUrl, HttpSession session, Model model) { // 验证用户凭证 User user = userService.authenticate(username, password); if (user != null) { // 创建全局会话 session.setAttribute(\"user\", user); // 生成JWT令牌 String token = JwtUtil.generateToken(user); // 存储令牌与会话的关联到Redis redisTemplate.opsForValue().set(\"sso:\" + token, session.getId()); // 重定向回原系统 return \"redirect:\" + redirectUrl + \"?token=\" + token; } return \"login\"; }}// 应用系统拦截器public class SsoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); // 检查本地会话 if (session != null && session.getAttribute(\"user\") != null) { return true; } // 检查请求中的JWT令牌 String token = request.getParameter(\"token\"); if (token != null && JwtUtil.validateToken(token)) { // 从Redis获取会话ID String sessionId = redisTemplate.opsForValue().get(\"sso:\" + token); if (sessionId != null) { // 创建本地会话 HttpSession newSession = request.getSession(); newSession.setAttribute(\"user\", getUserFromToken(token)); return true; } } // 重定向到认证中心 response.sendRedirect(authCenterUrl + \"?redirectUrl=\" + currentUrl); return false; }}

扩展建议

  1. 安全性增强:使用HTTPS传输,添加CSRF保护,设置合理的JWT过期时间
  2. 性能优化:使用Redis集群提高会话存储性能,添加本地会话缓存
  3. 用户体验:实现无缝跳转,提供统一的登录/登出界面
  4. 监控管理:添加会话监控和管理功能,支持强制下线

这个演示展示了SSO的基本流程和实现方式,实际项目中需要根据具体需求进行调整和完善。