基于Spring Security实现用户最后登录时间戳存储方案咨询
Alright, let's tackle this problem step by step. You want to record a user's last login timestamp only once when they log in, not on every page refresh or request. Since your app is stateless (using SessionCreationPolicy.STATELESS), we can't rely on traditional session tracking—instead, we need to hook into the authentication success event to update the timestamp exactly once per login.
Step 1: Add the Last Login Timestamp Field to Your User Entity
First, update your UserEntity to include the timestamp field, along with its getter/setter. Don't forget to update your database schema (e.g., add a last_login_timestamp column) to store this value.
public class UserEntity implements UserDetails { // Your existing fields private Integer pdmUid; private Role role; private boolean onlineSystem; // New field for login timestamp private LocalDateTime lastLoginTimestamp; // Getter and Setter public LocalDateTime getLastLoginTimestamp() { return lastLoginTimestamp; } public UserEntity setLastLoginTimestamp(LocalDateTime lastLoginTimestamp) { this.lastLoginTimestamp = lastLoginTimestamp; return this; } // Keep your existing methods (like authorities, username, etc.) }
Step 2: Update UserService to Handle Timestamp Updates
Add a method to your UserService that updates the timestamp for a given user:
@Service public class UserService { private final UserRepository userRepository; // Constructor injection public UserService(UserRepository userRepository) { this.userRepository = userRepository; } // Your existing methods (createIfNotExist, etc.) public void updateLastLoginTimestamp(Integer pdmUid) { UserEntity user = userRepository.findByPdmUid(pdmUid) .orElseThrow(() -> new RuntimeException("User not found with pdmUid: " + pdmUid)); user.setLastLoginTimestamp(LocalDateTime.now()); userRepository.save(user); } }
Step 3: Hook into Authentication Success to Trigger the Update
Since you're using a custom authentication setup, we'll create an AuthenticationSuccessHandler to run the timestamp update only when the user successfully logs in (not on every subsequent request).
@Component public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler { private final UserService userService; public CustomAuthSuccessHandler(UserService userService) { this.userService = userService; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // Extract the authenticated user's details UserEntity user = (UserEntity) authentication.getPrincipal(); // Update the login timestamp once userService.updateLastLoginTimestamp(user.getPdmUid()); // Continue with your existing post-login logic (e.g., generate JWT, send response) // ... } }
Then, register this handler in your SecurityConfiguration to tie it to your authentication flow:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final ApifyAuthConfigurer authConfigurer; private final PrincipalProvider principalProvider; private final CustomAuthSuccessHandler customAuthSuccessHandler; // Inject the handler // Update constructor to include the handler public SecurityConfiguration(ApifyAuthConfigurer authConfigurer, PrincipalProvider principalProvider, CustomAuthSuccessHandler customAuthSuccessHandler) { this.authConfigurer = authConfigurer; this.principalProvider = principalProvider; this.customAuthSuccessHandler = customAuthSuccessHandler; } @Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(new ApifyAuthenticationEntryPoint()) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // Attach the success handler to your authentication method (adjust if using OAuth2/JWT login) .formLogin() .successHandler(customAuthSuccessHandler); authConfigurer.configure(http, principalProvider); } }
Step 4: Fix the PrincipalProvider to Avoid Unnecessary Updates
Your current PrincipalProvider runs on every request (since it parses JWT claims each time). We'll modify it to fetch the latest user data from the database instead of creating a new UserEntity—this ensures you can return the stored login timestamp in the /current endpoint.
public class PrincipalProvider implements UserPrincipalProvider<UserDetails> { private final UserService userService; public PrincipalProvider(UserService userService) { this.userService = userService; } @Override public Optional<UserDetails> getPrincipal(Claims claims) { log.debug("#getPrincipal - {}", claims); Integer pdmUid = (Integer) claims.get("pdmUid"); // Create user if they don't exist (existing logic) userService.createIfNotExist(claims); // Fetch the full user entity from DB to get the latest login timestamp UserEntity user = userService.getUserByPdmUid(pdmUid); // Add this method to UserService return Optional.of(user); } }
Step 5: Update the /current Endpoint to Return the Timestamp
Modify your UserDto to include the timestamp, then update the controller to return it:
// UserDto class public class UserDto { private Integer pdmUid; private String role; private boolean onlineSystem; private LocalDateTime lastLoginTimestamp; // New field // Constructor including the timestamp public UserDto(Integer pdmUid, String role, boolean onlineSystem, LocalDateTime lastLoginTimestamp) { this.pdmUid = pdmUid; this.role = role; this.onlineSystem = onlineSystem; this.lastLoginTimestamp = lastLoginTimestamp; } // Getters } // Controller update @GetMapping("/current") public ResponseEntity<UserDto> getCurrent() { Optional<UserDetails> currentFromSecurity = userSecurityService.getCurrentFromSecurity(); if (!currentFromSecurity.isPresent()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } else { UserEntity userEntity = (UserEntity) currentFromSecurity.get(); return ResponseEntity.ok(new UserDto( userEntity.getPdmUid(), userEntity.getRole().name(), userEntity.isOnlineSystem(), userEntity.getLastLoginTimestamp() )); } }
Why This Works
- The timestamp is only updated once per login (when the authentication success handler runs), not on every request.
- Since your app is stateless, we rely on the authentication event to trigger the update—this aligns with your requirement to avoid updating on page refreshes or navigation.
- Fetching the user from the database in
PrincipalProviderensures you always have the latest login timestamp to return to the frontend.
内容的提问来源于stack exchange,提问作者okey1992

