Angular+REST后端会话管理需求及Spring Boot+Map方案咨询
Hey there! I totally get where you're coming from—OAuth2 + JWT can feel like overkill when your authorization and resource servers are part of the same app, especially since you're starting with OTP-based auth. Let's walk through a clean, lightweight session management implementation using Spring Boot with a simple map-based approach, tailored exactly for your use case.
Since your app's auth and resource layers live together, server-side session tracking is a perfect fit. We'll use a thread-safe map to store user sessions after OTP verification, pair it with HttpOnly cookies for secure session ID transmission, and add guards to validate sessions on every subsequent request.
1. Define a Session Entity
First, create a class to hold session details (user info, timestamps):
public class UserSession { private String userId; private String username; private LocalDateTime createdAt; private LocalDateTime expiresAt; // Constructor, getters, and setters public UserSession(String userId, String username, LocalDateTime createdAt, LocalDateTime expiresAt) { this.userId = userId; this.username = username; this.createdAt = createdAt; this.expiresAt = expiresAt; } // Add getters for all fields }
2. Build a Thread-Safe Session Manager
Use ConcurrentHashMap to handle concurrent session access, plus methods to create, refresh, invalidate, and clean up sessions:
@Component public class SessionManager { private final ConcurrentHashMap<String, UserSession> activeSessions = new ConcurrentHashMap<>(); private static final int SESSION_EXPIRY_MINUTES = 30; // Create a new session after OTP verification public String createSession(String userId, String username) { String sessionId = UUID.randomUUID().toString(); LocalDateTime now = LocalDateTime.now(); UserSession session = new UserSession( userId, username, now, now.plusMinutes(SESSION_EXPIRY_MINUTES) ); activeSessions.put(sessionId, session); return sessionId; } // Fetch session and refresh its expiry time on each valid request public UserSession getAndRefreshSession(String sessionId) { UserSession session = activeSessions.get(sessionId); if (session != null && !isSessionExpired(session)) { session.setExpiresAt(LocalDateTime.now().plusMinutes(SESSION_EXPIRY_MINUTES)); return session; } // Remove expired/invalid sessions immediately activeSessions.remove(sessionId); return null; } // Invalidate session on logout public void invalidateSession(String sessionId) { activeSessions.remove(sessionId); } // Helper to check if a session is expired private boolean isSessionExpired(UserSession session) { return LocalDateTime.now().isAfter(session.getExpiresAt()); } // Scheduled task to clean up expired sessions (runs every minute) @Scheduled(fixedRate = 60000) public void cleanExpiredSessions() { activeSessions.entrySet().removeIf(entry -> isSessionExpired(entry.getValue())); } }
3. Integrate with OTP Authentication
After validating the OTP, create a session and send the session ID back via an HttpOnly cookie:
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private SessionManager sessionManager; @Autowired private OtpService otpService; // Your existing OTP validation service @Autowired private UserRepository userRepository; // Your user data repository @PostMapping("/verify-otp") public ResponseEntity<?> verifyOtp( @RequestBody OtpVerificationRequest request, HttpServletResponse response ) { // Validate the provided OTP if (!otpService.validateOtp(request.getPhoneNumber(), request.getOtp())) { return ResponseEntity.badRequest().body("Invalid OTP"); } // Fetch user details from your database User user = userRepository.findByPhoneNumber(request.getPhoneNumber()) .orElseThrow(() -> new RuntimeException("User not found")); // Create a new session String sessionId = sessionManager.createSession(user.getId(), user.getUsername()); // Set HttpOnly cookie (prevents XSS attacks) Cookie sessionCookie = new Cookie("APP_SESSION_ID", sessionId); sessionCookie.setHttpOnly(true); sessionCookie.setSecure(true); // Enable in production (HTTPS only) sessionCookie.setPath("/"); sessionCookie.setMaxAge(SESSION_EXPIRY_MINUTES * 60); // Match session expiry response.addCookie(sessionCookie); return ResponseEntity.ok("OTP verified. Session started."); } @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletRequest request, HttpServletResponse response) { // Find and invalidate the session ID from cookies Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("APP_SESSION_ID".equals(cookie.getName())) { sessionManager.invalidateSession(cookie.getValue()); // Clear the cookie from the client cookie.setValue(""); cookie.setPath("/"); cookie.setMaxAge(0); response.addCookie(cookie); break; } } } return ResponseEntity.ok("Logged out successfully."); } }
4. Add a Session Validation Interceptor
Create an interceptor to check for valid sessions on all non-auth endpoints:
@Component public class SessionValidationInterceptor implements HandlerInterceptor { @Autowired private SessionManager sessionManager; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Skip validation for auth endpoints String requestPath = request.getRequestURI(); if (requestPath.startsWith("/auth/")) { return true; } // Extract session ID from cookies String sessionId = null; Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("APP_SESSION_ID".equals(cookie.getName())) { sessionId = cookie.getValue(); break; } } } // Validate session existence and expiry if (sessionId == null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No active session found."); return false; } UserSession session = sessionManager.getAndRefreshSession(sessionId); if (session == null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session expired or invalid."); return false; } // Attach user info to the request context for downstream use request.setAttribute("LOGGED_IN_USER_ID", session.getUserId()); request.setAttribute("LOGGED_IN_USERNAME", session.getUsername()); return true; } }
5. Register the Interceptor
Add the interceptor to your Spring Boot web configuration:
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private SessionValidationInterceptor sessionInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(sessionInterceptor) .addPathPatterns("/**") .excludePathPatterns("/auth/**"); } }
- Security: Using HttpOnly cookies prevents XSS attacks from stealing session IDs. Enable
Securemode in production to ensure cookies are only sent over HTTPS. - Persistence: For production environments where server restarts can't lose sessions, replace the
ConcurrentHashMapwith Redis. The SessionManager logic will stay almost identical—just swap map operations with RedisTemplate calls. - CSRF Protection: Since you're using cookies, enable Spring's CSRF protection (it's on by default in Spring Security) to prevent cross-site request forgery.
- Angular Integration: Your Angular app doesn't need extra code to handle cookies—browsers automatically attach them to same-domain requests. If your frontend is on a different domain, configure CORS in Spring to allow credentials.
内容的提问来源于stack exchange,提问作者osama yaccoub

