不使用Spring Security实现Spring Boot认证及多标签页同步登录登出
不依赖Spring Security实现Spring Boot+React多标签页认证同步
一、先修复后端代码的核心问题
你的现有代码存在逻辑错误和线程安全隐患,先修正这些问题才能保证认证功能正常:
- 线程安全问题:
activeSessions用HashMap在多请求并发场景下会出现线程安全问题,替换为ConcurrentHashMap。 identificateSession方法冗余:直接用Map的get方法即可,无需遍历。- Logout逻辑错误:原代码中
activeSessions.remove(exitUser)是按value删除,而你的Map是key为SessionID、value为用户名,这行逻辑完全错误,应该按SessionID删除。
修正后的Controller代码:
import com.cleanyco.weblab4backend.model.User; import com.cleanyco.weblab4backend.repository.UserRepository; import com.cleanyco.weblab4backend.util.MD5PasswordEncoder; import com.google.gson.Gson; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; @RestController public class UserController { private static final Logger logger = Logger.getLogger(UserController.class.getName()); private final UserRepository userRepository; public UserController(@Autowired UserRepository userRepository) { this.userRepository = userRepository; } // 替换为线程安全的ConcurrentHashMap private final Map<String, String> activeSessions = new ConcurrentHashMap<>(); // 简化session校验逻辑 public String identificateSession(String sessionID) { return activeSessions.get(sessionID); } @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @PostMapping("/signup") public ResponseEntity<?> signup(HttpSession session, @RequestBody User user) { User existingUser = userRepository.findUserByUsername(user.getUsername()); if (existingUser == null) { logger.info("User wasn't found. Registering a new one..."); activeSessions.put(session.getId(), user.getUsername()); logger.info("Registered an active session. User: " + user.getUsername() + "; SessionID: " + session.getId()); user.setPassword(MD5PasswordEncoder.hash(user.getPassword())); userRepository.save(user); logger.info("User was successfully saved"); return new ResponseEntity<>(HttpStatus.OK); } else { logger.info("User with the same name already exists"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @PostMapping("/login") public ResponseEntity<?> login(HttpSession session, @RequestBody User user) { User existingUser = userRepository.findUserByUsername(user.getUsername()); if (existingUser != null) { String userPassword = user.getPassword(); String hashedPassword = MD5PasswordEncoder.hash(userPassword); if (existingUser.getPassword().equals(hashedPassword)) { logger.info("User successfully logged in"); // 如果session未关联用户,添加映射 if (identificateSession(session.getId()) == null) { activeSessions.put(session.getId(), user.getUsername()); } return new ResponseEntity<>(HttpStatus.OK); } else { logger.info("Invalid password was provided"); return new ResponseEntity<>(HttpStatus.FORBIDDEN); } } else { logger.info("User not found"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } private static final Gson gson = new Gson(); @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @GetMapping(value = "/checksession", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<?> getActiveSession(HttpSession session) { String username = identificateSession(session.getId()); if (username != null) { User user = userRepository.findUserByUsername(username); return ResponseEntity.ok(gson.toJson(user)); } else { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") @GetMapping("/logout") public ResponseEntity<?> logout(HttpSession session) { String sessionId = session.getId(); String exitUser = activeSessions.remove(sessionId); if (exitUser == null) { logger.info("An error occurred while logging out the user: user already logged out"); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } else { // 可选:使Session失效 session.invalidate(); logger.info("User was successfully logged out. User: " + exitUser + "; SessionID: " + sessionId); return new ResponseEntity<>(HttpStatus.OK); } } }
二、前端实现多标签页同步登录/登出
方式1:轮询方案(快速实现)
在React应用的根组件中,定时调用后端的/checksession接口,校验当前Session的登录状态,同步全局登录状态:
import { useEffect, useState } from 'react'; import axios from 'axios'; function App() { const [currentUser, setCurrentUser] = useState(null); // 初始化时检查登录状态,之后定时轮询 useEffect(() => { const checkSession = async () => { try { const response = await axios.get('http://localhost:8080/checksession', { withCredentials: true // 必须携带Cookie,否则SessionID无法传递 }); setCurrentUser(response.data); } catch (error) { setCurrentUser(null); } }; // 首次加载检查 checkSession(); // 每隔30秒检查一次 const interval = setInterval(checkSession, 30000); return () => clearInterval(interval); }, []); // 登录/登出操作的示例 const handleLogin = async (userData) => { try { await axios.post('http://localhost:8080/login', userData, { withCredentials: true }); // 登录成功后立即检查Session const sessionResponse = await axios.get('http://localhost:8080/checksession', { withCredentials: true }); setCurrentUser(sessionResponse.data); // 同步到sessionStorage,供其他标签页监听 sessionStorage.setItem('currentUser', JSON.stringify(sessionResponse.data)); } catch (error) { console.error('登录失败:', error); } }; const handleLogout = async () => { try { await axios.get('http://localhost:8080/logout', { withCredentials: true }); setCurrentUser(null); // 清除sessionStorage,触发其他标签页的监听 sessionStorage.removeItem('currentUser'); } catch (error) { console.error('登出失败:', error); } }; return ( <div className="App"> {currentUser ? ( <> <p>欢迎, {currentUser.username}</p> <button onClick={handleLogout}>登出</button> </> ) : ( <> {/* 登录/注册表单 */} <button onClick={() => handleLogin({ username: 'test', password: 'test' })}>登录</button> </> )} </div> ); } export default App;
方式2:Storage事件监听(实时同步,推荐)
利用浏览器的storage事件,同一浏览器的不同标签页可以监听sessionStorage/localStorage的变化,实现实时同步:
import { useEffect, useState } from 'react'; import axios from 'axios'; function App() { const [currentUser, setCurrentUser] = useState(null); // 初始化检查登录状态 useEffect(() => { const initSession = async () => { try { const response = await axios.get('http://localhost:8080/checksession', { withCredentials: true }); setCurrentUser(response.data); sessionStorage.setItem('currentUser', JSON.stringify(response.data)); } catch (error) { setCurrentUser(null); sessionStorage.removeItem('currentUser'); } }; initSession(); // 监听其他标签页的storage变化 const handleStorageChange = async (e) => { if (e.key === 'currentUser') { if (e.newValue) { setCurrentUser(JSON.parse(e.newValue)); } else { // 验证后端Session是否真的失效,防止前端篡改 try { await axios.get('http://localhost:8080/checksession', { withCredentials: true }); } catch { setCurrentUser(null); } } } }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, []); const handleLogin = async (userData) => { try { await axios.post('http://localhost:8080/login', userData, { withCredentials: true }); const sessionResponse = await axios.get('http://localhost:8080/checksession', { withCredentials: true }); setCurrentUser(sessionResponse.data); sessionStorage.setItem('currentUser', JSON.stringify(sessionResponse.data)); } catch (error) { console.error('登录失败:', error); } }; const handleLogout = async () => { try { await axios.get('http://localhost:8080/logout', { withCredentials: true }); setCurrentUser(null); sessionStorage.removeItem('currentUser'); } catch (error) { console.error('登出失败:', error); } }; return ( <div className="App"> {currentUser ? ( <> <p>欢迎, {currentUser.username}</p> <button onClick={handleLogout}>登出</button> </> ) : ( <> {/* 登录/注册表单 */} <button onClick={() => handleLogin({ username: 'test', password: 'test' })}>登录</button> </> )} </div> ); } export default App;
三、核心原理说明
- 浏览器Session共享:同一浏览器的多个标签页默认共享同一个HttpSession,所以第二个标签页打开时,请求会自动携带已登录的SessionID,后端
/checksession接口能直接识别用户。 - Storage事件同步:当一个标签页完成登录/登出时,修改
sessionStorage,其他标签页通过监听storage事件实时更新状态,避免轮询的延迟。 - 后端校验兜底:即使前端
sessionStorage被篡改,后端的/checksession接口会基于真实的Session状态返回结果,保证认证的安全性。
内容的提问来源于stack exchange,提问作者grxuau
相关产品推荐
相关产品推荐

