You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular JS+Spring Boot Web应用并发登录检测及处理方案问询

Got it, let's walk through how to build concurrent login detection with user-controlled actions in your AngularJS + Spring Boot app. This approach gives users control over their sessions while keeping your app secure.

Core Concept

The key is to track each user's active sessions (using a persistent store like Redis for scalability). When a new login request comes in, we check if the user already has an active session. If yes, we prompt the new login user to choose between:

  • Forcing the old session to log out and proceeding with the new login
  • Canceling the new login and leaving the old session intact

Backend (Spring Boot) Implementation

1. Session Tracking Setup

Use Redis to store active sessions for each user. We'll map user IDs to a list of valid session IDs (we can use JWT's jti claim as the unique session ID for each login).

2. Modified Login Endpoint

Update your login API to check for existing active sessions before completing the login:

@PostMapping("/api/auth/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
    // Validate user credentials (username/password)
    UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
    if (!passwordEncoder.matches(loginRequest.getPassword(), userDetails.getPassword())) {
        return ResponseEntity.badRequest().body("Invalid credentials");
    }

    User user = userRepository.findByUsername(loginRequest.getUsername());
    String userSessionKey = "user:sessions:" + user.getId();
    
    // Check if user has active sessions
    List<String> activeSessions = redisTemplate.opsForList().range(userSessionKey, 0, -1);
    if (!activeSessions.isEmpty()) {
        // Return response indicating concurrent login needs user action
        ConcurrentLoginResponse response = new ConcurrentLoginResponse();
        response.setHasActiveSession(true);
        response.setOldSessionId(activeSessions.get(0)); // Track the most recent old session
        response.setMessage("Your account is logged in on another device. Choose an action.");
        response.setUserId(user.getId());
        return ResponseEntity.status(HttpStatus.ACCEPTED).body(response);
    }

    // No active sessions: proceed with normal login
    String sessionId = UUID.randomUUID().toString();
    String jwtToken = jwtUtil.generateToken(userDetails, sessionId);
    
    // Store new session in Redis
    redisTemplate.opsForList().rightPush(userSessionKey, sessionId);
    
    return ResponseEntity.ok(new AuthResponse(jwtToken));
}

3. Action Handling Endpoints

Create two endpoints to handle the user's choice:

Force Logout Old Session

@PostMapping("/api/auth/force-logout-old")
public ResponseEntity<?> forceLogoutOldSession(@RequestBody ConcurrentLoginActionRequest request) {
    String userSessionKey = "user:sessions:" + request.getUserId();
    
    // Remove the old session from Redis
    redisTemplate.opsForList().remove(userSessionKey, 1, request.getOldSessionId());
    
    // Generate new token for the new login
    User user = userRepository.findById(request.getUserId()).orElseThrow();
    UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
    String newSessionId = UUID.randomUUID().toString();
    String newJwtToken = jwtUtil.generateToken(userDetails, newSessionId);
    
    // Store new session
    redisTemplate.opsForList().rightPush(userSessionKey, newSessionId);
    
    return ResponseEntity.ok(new AuthResponse(newJwtToken));
}

Cancel New Login

@PostMapping("/api/auth/cancel-new-login")
public ResponseEntity<?> cancelNewLogin() {
    return ResponseEntity.ok("New login canceled. Old session remains active.");
}

4. Session Validation Interceptor

Add an interceptor to check if the current session is still valid on every request:

@Component
public class SessionValidationInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        String token = authHeader.substring(7);
        String userId = jwtUtil.extractUserId(token);
        String sessionId = jwtUtil.extractJti(token);
        
        String userSessionKey = "user:sessions:" + userId;
        List<String> activeSessions = redisTemplate.opsForList().range(userSessionKey, 0, -1);
        
        if (!activeSessions.contains(sessionId)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        return true;
    }
}

Don't forget to register the interceptor in your Spring config to apply it to all API requests.


Frontend (AngularJS) Implementation

1. Login Controller with Concurrent Login Prompt

Modify your login controller to handle the 202 Accepted response and prompt the user with a modal:

angular.module('yourApp')
.controller('LoginController', ['$scope', '$http', '$uibModal', '$location', function($scope, $http, $uibModal, $location) {
    $scope.login = function() {
        $http.post('/api/auth/login', {
            username: $scope.username,
            password: $scope.password
        }).then(function(response) {
            if (response.status === 202) {
                // Show concurrent login choice modal
                var modalInstance = $uibModal.open({
                    templateUrl: 'concurrent-login-modal.html',
                    controller: 'ConcurrentLoginModalCtrl',
                    resolve: {
                        sessionData: function() {
                            return response.data;
                        }
                    }
                });

                modalInstance.result.then(function(action) {
                    if (action === 'force') {
                        // Call force logout endpoint
                        $http.post('/api/auth/force-logout-old', {
                            userId: response.data.userId,
                            oldSessionId: response.data.oldSessionId
                        }).then(function(res) {
                            localStorage.setItem('authToken', res.data.token);
                            $location.path('/dashboard');
                        });
                    } else if (action === 'cancel') {
                        $scope.errorMsg = 'Login canceled. Your old session is still active.';
                    }
                });
            } else {
                // Normal login success
                localStorage.setItem('authToken', response.data.token);
                $location.path('/dashboard');
            }
        }).catch(function(error) {
            $scope.errorMsg = 'Invalid username or password';
        });
    };
}])
.controller('ConcurrentLoginModalCtrl', ['$scope', '$uibModalInstance', 'sessionData', function($scope, $uibModalInstance, sessionData) {
    $scope.sessionData = sessionData;

    $scope.forceLogout = function() {
        $uibModalInstance.close('force');
    };

    $scope.cancelLogin = function() {
        $uibModalInstance.close('cancel');
    };
}]);

2. Concurrent Login Modal Template (concurrent-login-modal.html)

<div class="modal-header">
    <h3>Concurrent Login Detected</h3>
</div>
<div class="modal-body">
    <p>{{ sessionData.message }}</p>
</div>
<div class="modal-footer">
    <button class="btn btn-danger" ng-click="forceLogout()">Force Logout Old Session</button>
    <button class="btn btn-default" ng-click="cancelLogin()">Cancel This Login</button>
</div>

3. Global HTTP Interceptor for Session Expiry

Add an interceptor to handle 401 Unauthorized responses (triggered when the old session is forced out):

angular.module('yourApp')
.factory('authInterceptor', ['$q', '$location', function($q, $location) {
    return {
        responseError: function(response) {
            if (response.status === 401) {
                localStorage.removeItem('authToken');
                $location.path('/login');
                alert('Your session has been terminated by a new login. Please log in again.');
            }
            return $q.reject(response);
        }
    };
}])
.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('authInterceptor');
}]);

End-to-End Flow

  1. User logs in on Device A: Backend stores their session in Redis and returns a JWT.
  2. User attempts to log in on Device B: Backend detects an active session, returns a 202 response.
  3. Device B shows the modal with two options.
    • If user chooses "Force Logout": Backend removes Device A's session from Redis, returns a new JWT to Device B. Device A's next request gets a 401 and redirects to login.
    • If user chooses "Cancel": Device B stays on the login page, Device A's session remains active.

内容的提问来源于stack exchange,提问作者user1797298

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:32:36