Design User Login System Guide

System Overview and Architecture

A robust user login system forms the backbone of secure web applications, handling authentication (verifying user identity) and authorization (controlling access to resources). This guide presents a production-ready architecture that balances security, scalability, and maintainability.

Core Components Architecture


graph TB
A[User Browser] --> B[UserLoginWebsite]
B --> C[AuthenticationFilter]
C --> D[UserLoginService]
D --> E[Redis Session Store]
D --> F[UserService]
D --> G[PermissionService]

subgraph "External Services"
    F[UserService]
    G[PermissionService]
end

subgraph "Session Management"
    E[Redis Session Store]
    H[JWT Token Service]
end

subgraph "Web Layer"
    B[UserLoginWebsite]
    C[AuthenticationFilter]
end

subgraph "Business Layer"
    D[UserLoginService]
end

Design Philosophy: The architecture follows the separation of concerns principle, with each component having a single responsibility. The web layer handles HTTP interactions, the business layer manages authentication logic, and external services provide user data and permissions.

UserLoginWebsite Component

The UserLoginWebsite serves as the presentation layer, providing both user-facing login interfaces and administrative user management capabilities.

Key Responsibilities

  • User Interface: Render login forms, dashboard, and user profile pages
  • Admin Interface: Provide user management tools for administrators
  • Session Handling: Manage cookies and client-side session state
  • Security Headers: Implement CSRF protection and secure cookie settings

Implementation Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Controller
@RequestMapping("/auth")
public class AuthController {

@Autowired
private UserLoginService userLoginService;

@PostMapping("/login")
public ResponseEntity<LoginResponse> login(
@RequestBody LoginRequest request,
HttpServletResponse response) {

try {
LoginResult result = userLoginService.authenticate(
request.getUsername(),
request.getPassword()
);

// Set secure cookie with session ID
Cookie sessionCookie = new Cookie("JSESSIONID", result.getSessionId());
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(true);
sessionCookie.setPath("/");
sessionCookie.setMaxAge(3600); // 1 hour
response.addCookie(sessionCookie);

return ResponseEntity.ok(new LoginResponse(result.getUser()));

} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new LoginResponse("Invalid credentials"));
}
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
String sessionId = extractSessionId(request);
userLoginService.logout(sessionId);
return ResponseEntity.ok().build();
}
}

Interview Insight: “How do you handle CSRF attacks in login systems?”

Answer: Implement CSRF tokens for state-changing operations, use SameSite cookie attributes, and validate the Origin/Referer headers. The login form should include a CSRF token that’s validated on the server side.

UserLoginService Component

The UserLoginService acts as the core business logic layer, orchestrating authentication workflows and session management.

Design Philosophy

The service follows the facade pattern, providing a unified interface for complex authentication operations while delegating specific tasks to specialized components.

Core Operations Flow


sequenceDiagram
participant C as Client
participant ULS as UserLoginService
participant US as UserService
participant PS as PermissionService
participant R as Redis

C->>ULS: authenticate(username, password)
ULS->>US: validateCredentials(username, password)
US-->>ULS: User object
ULS->>PS: getUserPermissions(userId)
PS-->>ULS: Permissions list
ULS->>R: storeSession(sessionId, userInfo)
R-->>ULS: confirmation
ULS-->>C: LoginResult with sessionId

Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Service
@Transactional
public class UserLoginService {

@Autowired
private UserService userService;

@Autowired
private PermissionService permissionService;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private JwtTokenService jwtTokenService;

private static final String SESSION_PREFIX = "user:session:";
private static final int SESSION_TIMEOUT = 3600; // 1 hour

public LoginResult authenticate(String username, String password) {
// Step 1: Validate credentials
User user = userService.validateCredentials(username, password);
if (user == null) {
throw new AuthenticationException("Invalid credentials");
}

// Step 2: Load user permissions
List<Permission> permissions = permissionService.getUserPermissions(user.getId());

// Step 3: Create session
String sessionId = generateSessionId();
UserSession session = new UserSession(user, permissions, System.currentTimeMillis());

// Step 4: Store session in Redis
redisTemplate.opsForValue().set(
SESSION_PREFIX + sessionId,
session,
SESSION_TIMEOUT,
TimeUnit.SECONDS
);

// Step 5: Generate JWT token (optional)
String jwtToken = jwtTokenService.generateToken(user, permissions);

return new LoginResult(sessionId, user, jwtToken);
}

public void logout(String sessionId) {
redisTemplate.delete(SESSION_PREFIX + sessionId);
}

public UserSession getSession(String sessionId) {
return (UserSession) redisTemplate.opsForValue().get(SESSION_PREFIX + sessionId);
}

public void refreshSession(String sessionId) {
redisTemplate.expire(SESSION_PREFIX + sessionId, SESSION_TIMEOUT, TimeUnit.SECONDS);
}

private String generateSessionId() {
return UUID.randomUUID().toString().replace("-", "");
}
}

Interview Insight: “How do you handle concurrent login attempts?”

Answer: Implement rate limiting using Redis counters, track failed login attempts per IP/username, and use exponential backoff. Consider implementing account lockout policies and CAPTCHA after multiple failed attempts.

Redis Session Management

Redis serves as the distributed session store, providing fast access to session data across multiple application instances.

Session Storage Strategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class RedisSessionManager {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final String USER_SESSION_PREFIX = "session:user:";
private static final String USER_PERMISSIONS_PREFIX = "session:permissions:";
private static final int DEFAULT_TIMEOUT = 1800; // 30 minutes

public void storeUserSession(String sessionId, UserSession session) {
String userKey = USER_SESSION_PREFIX + sessionId;
String permissionsKey = USER_PERMISSIONS_PREFIX + sessionId;

// Store user info and permissions separately for optimized access
redisTemplate.opsForHash().putAll(userKey, session.toMap());
redisTemplate.opsForSet().add(permissionsKey, session.getPermissions().toArray());

// Set expiration
redisTemplate.expire(userKey, DEFAULT_TIMEOUT, TimeUnit.SECONDS);
redisTemplate.expire(permissionsKey, DEFAULT_TIMEOUT, TimeUnit.SECONDS);
}

public UserSession getUserSession(String sessionId) {
String userKey = USER_SESSION_PREFIX + sessionId;
Map<Object, Object> sessionData = redisTemplate.opsForHash().entries(userKey);

if (sessionData.isEmpty()) {
return null;
}

// Refresh session timeout on access
redisTemplate.expire(userKey, DEFAULT_TIMEOUT, TimeUnit.SECONDS);
redisTemplate.expire(USER_PERMISSIONS_PREFIX + sessionId, DEFAULT_TIMEOUT, TimeUnit.SECONDS);

return UserSession.fromMap(sessionData);
}
}

Session Cleanup Strategy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Slf4j
public class SessionCleanupService {

@Scheduled(fixedRate = 300000) // Every 5 minutes
public void cleanupExpiredSessions() {
Set<String> expiredSessions = findExpiredSessions();

for (String sessionId : expiredSessions) {
cleanupSession(sessionId);
}

log.info("Cleaned up {} expired sessions", expiredSessions.size());
}

private void cleanupSession(String sessionId) {
redisTemplate.delete(USER_SESSION_PREFIX + sessionId);
redisTemplate.delete(USER_PERMISSIONS_PREFIX + sessionId);
}
}

Interview Insight: “How do you handle Redis failures in session management?”

Answer: Implement fallback mechanisms like database session storage, use Redis clustering for high availability, and implement circuit breakers. Consider graceful degradation where users are redirected to re-login if session data is unavailable.

AuthenticationFilter Component

The AuthenticationFilter acts as a security gateway, validating every HTTP request to ensure proper authentication and authorization.

Filter Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Component
@Order(1)
public class AuthenticationFilter implements Filter {

@Autowired
private UserLoginService userLoginService;

@Autowired
private PermissionService permissionService;

private static final Set<String> EXCLUDED_PATHS = Set.of(
"/auth/login", "/auth/register", "/public", "/health"
);

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

String requestPath = httpRequest.getRequestURI();

// Skip authentication for excluded paths
if (isExcludedPath(requestPath)) {
chain.doFilter(request, response);
return;
}

try {
// Extract session ID from cookie
String sessionId = extractSessionId(httpRequest);
if (sessionId == null) {
handleUnauthorized(httpResponse, "No session found");
return;
}

// Validate session
UserSession session = userLoginService.getSession(sessionId);
if (session == null || isSessionExpired(session)) {
handleUnauthorized(httpResponse, "Session expired");
return;
}

// Check permissions for the requested resource
if (!hasPermission(session, requestPath, httpRequest.getMethod())) {
handleForbidden(httpResponse, "Insufficient permissions");
return;
}

// Refresh session timeout
userLoginService.refreshSession(sessionId);

// Set user context for downstream processing
SecurityContextHolder.setContext(new SecurityContext(session.getUser()));

chain.doFilter(request, response);

} catch (Exception e) {
log.error("Authentication filter error", e);
handleUnauthorized(httpResponse, "Authentication error");
} finally {
SecurityContextHolder.clearContext();
}
}

private boolean hasPermission(UserSession session, String path, String method) {
return permissionService.checkPermission(
session.getUser().getId(),
path,
method
);
}

private void handleUnauthorized(HttpServletResponse response, String message)
throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"" + message + "\"}");
}
}

Interview Insight: “How do you optimize filter performance for high-traffic applications?”

Answer: Cache permission checks in Redis, use efficient data structures for path matching, implement request batching for permission validation, and consider using async processing for non-blocking operations.

JWT Integration Strategy

JWT (JSON Web Tokens) can complement session-based authentication by providing stateless authentication capabilities and enabling distributed systems integration.

When to Use JWT

Use JWT when:

  • Building microservices architecture
  • Implementing single sign-on (SSO)
  • Supporting mobile applications
  • Enabling API authentication
  • Requiring stateless authentication

Use Sessions when:

  • Building traditional web applications
  • Requiring immediate token revocation
  • Handling sensitive operations
  • Managing complex user states

Hybrid Approach Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class JwtTokenService {

@Value("${jwt.secret}")
private String jwtSecret;

@Value("${jwt.expiration}")
private int jwtExpiration;

public String generateToken(User user, List<Permission> permissions) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("permissions", permissions.stream()
.map(Permission::getName)
.collect(Collectors.toList()));

return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration * 1000))
.signWith(SignatureAlgorithm.HS256, jwtSecret)
.compact();
}

public Claims validateToken(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
throw new AuthenticationException("Invalid JWT token", e);
}
}

public boolean isTokenExpired(String token) {
Date expiration = validateToken(token).getExpiration();
return expiration.before(new Date());
}
}

JWT vs Session Comparison

Aspect JWT Session
State Stateless Stateful
Revocation Difficult Immediate
Scalability High Medium
Security Token-based Server-side
Complexity Medium Low
Mobile Support Excellent Good

Interview Insight: “How do you handle JWT token revocation?”

Answer: Implement a token blacklist in Redis, use short-lived tokens with refresh mechanism, maintain a token version number in the database, and implement token rotation strategies.

Security Best Practices

Password Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class PasswordSecurityService {

private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12);

public String hashPassword(String plainPassword) {
return passwordEncoder.encode(plainPassword);
}

public boolean verifyPassword(String plainPassword, String hashedPassword) {
return passwordEncoder.matches(plainPassword, hashedPassword);
}

public boolean isPasswordStrong(String password) {
return password.length() >= 8 &&
password.matches(".*[A-Z].*") &&
password.matches(".*[a-z].*") &&
password.matches(".*[0-9].*") &&
password.matches(".*[!@#$%^&*()].*");
}
}

Rate Limiting Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class RateLimitingService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final String RATE_LIMIT_PREFIX = "rate_limit:";
private static final int MAX_ATTEMPTS = 5;
private static final int WINDOW_SECONDS = 300; // 5 minutes

public boolean isRateLimited(String identifier) {
String key = RATE_LIMIT_PREFIX + identifier;
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);

if (attempts == null) {
redisTemplate.opsForValue().set(key, 1, WINDOW_SECONDS, TimeUnit.SECONDS);
return false;
}

if (attempts >= MAX_ATTEMPTS) {
return true;
}

redisTemplate.opsForValue().increment(key);
return false;
}
}

User Session Lifecycle Management

Session Creation Flow


flowchart TD
A[User Login Request] --> B{Validate Credentials}
B -->|Invalid| C[Return Error]
B -->|Valid| D[Load User Permissions]
D --> E[Generate Session ID]
E --> F[Create JWT Token]
F --> G[Store Session in Redis]
G --> H[Set Secure Cookie]
H --> I[Return Success Response]

style A fill:#e1f5fe
style I fill:#c8e6c9
style C fill:#ffcdd2

Session Validation Process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class SessionValidator {

public ValidationResult validateSession(String sessionId, String requestPath) {
// Step 1: Check session existence
UserSession session = getSessionFromRedis(sessionId);
if (session == null) {
return ValidationResult.failure("Session not found");
}

// Step 2: Check session expiration
if (isSessionExpired(session)) {
cleanupSession(sessionId);
return ValidationResult.failure("Session expired");
}

// Step 3: Validate user status
if (!isUserActive(session.getUser())) {
return ValidationResult.failure("User account disabled");
}

// Step 4: Check resource permissions
if (!hasResourcePermission(session, requestPath)) {
return ValidationResult.failure("Insufficient permissions");
}

return ValidationResult.success(session);
}

private boolean isSessionExpired(UserSession session) {
long currentTime = System.currentTimeMillis();
long sessionTime = session.getLastAccessTime();
return (currentTime - sessionTime) > SESSION_TIMEOUT_MS;
}
}

Error Handling and Logging

Comprehensive Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@ControllerAdvice
public class AuthenticationExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(AuthenticationExceptionHandler.class);

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthenticationException(
AuthenticationException e, HttpServletRequest request) {

// Log security event
logger.warn("Authentication failed for IP: {} - {}",
getClientIpAddress(request), e.getMessage());

return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Authentication failed", "AUTH_001"));
}

@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<ErrorResponse> handleAuthorizationException(
AuthorizationException e, HttpServletRequest request) {

// Log authorization event
logger.warn("Authorization failed for user: {} on resource: {}",
getCurrentUser(), request.getRequestURI());

return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("Access denied", "AUTH_002"));
}
}

Performance Optimization Strategies

Caching Strategies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class PermissionCacheService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final String PERMISSION_CACHE_PREFIX = "permissions:user:";
private static final int CACHE_TTL = 600; // 10 minutes

@Cacheable(value = "userPermissions", key = "#userId")
public List<Permission> getUserPermissions(Long userId) {
String cacheKey = PERMISSION_CACHE_PREFIX + userId;
List<Permission> permissions = (List<Permission>) redisTemplate.opsForValue().get(cacheKey);

if (permissions == null) {
permissions = permissionService.loadUserPermissions(userId);
redisTemplate.opsForValue().set(cacheKey, permissions, CACHE_TTL, TimeUnit.SECONDS);
}

return permissions;
}

@CacheEvict(value = "userPermissions", key = "#userId")
public void invalidateUserPermissions(Long userId) {
redisTemplate.delete(PERMISSION_CACHE_PREFIX + userId);
}
}

Monitoring and Alerting

Security Metrics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
public class SecurityMetricsCollector {

private final MeterRegistry meterRegistry;
private final Counter loginAttempts;
private final Counter loginFailures;
private final Timer authenticationTime;

public SecurityMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.loginAttempts = Counter.builder("login.attempts")
.description("Total login attempts")
.register(meterRegistry);
this.loginFailures = Counter.builder("login.failures")
.description("Failed login attempts")
.register(meterRegistry);
this.authenticationTime = Timer.builder("authentication.time")
.description("Authentication processing time")
.register(meterRegistry);
}

public void recordLoginAttempt() {
loginAttempts.increment();
}

public void recordLoginFailure(String reason) {
loginFailures.increment(Tags.of("reason", reason));
}

public Timer.Sample startAuthenticationTimer() {
return Timer.start(meterRegistry);
}
}

Production Deployment Considerations

High Availability Setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Redis Cluster Configuration
redis:
cluster:
nodes:
- redis-node1:6379
- redis-node2:6379
- redis-node3:6379
max-redirects: 3
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0

Load Balancer Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream auth_backend {
server auth-service-1:8080;
server auth-service-2:8080;
server auth-service-3:8080;
}

server {
listen 443 ssl;
server_name auth.example.com;

location /auth {
proxy_pass http://auth_backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Testing Strategies

Integration Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@SpringBootTest
@AutoConfigureTestDatabase
class UserLoginServiceIntegrationTest {

@Autowired
private UserLoginService userLoginService;

@MockBean
private UserService userService;

@Test
void shouldAuthenticateValidUser() {
// Given
User mockUser = createMockUser();
when(userService.validateCredentials("testuser", "password"))
.thenReturn(mockUser);

// When
LoginResult result = userLoginService.authenticate("testuser", "password");

// Then
assertThat(result.getSessionId()).isNotNull();
assertThat(result.getUser().getUsername()).isEqualTo("testuser");
}

@Test
void shouldRejectInvalidCredentials() {
// Given
when(userService.validateCredentials("testuser", "wrongpassword"))
.thenReturn(null);

// When & Then
assertThatThrownBy(() -> userLoginService.authenticate("testuser", "wrongpassword"))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Invalid credentials");
}
}

Common Interview Questions and Answers

Q: How do you handle session fixation attacks?

A: Generate a new session ID after successful authentication, invalidate the old session, and ensure session IDs are cryptographically secure. Implement proper session lifecycle management.

Q: What’s the difference between authentication and authorization?

A: Authentication verifies who you are (identity), while authorization determines what you can do (permissions). Authentication happens first, followed by authorization for each resource access.

Q: How do you implement “Remember Me” functionality securely?

A: Use a separate persistent token stored in a secure cookie, implement token rotation, store tokens with expiration dates, and provide users with the ability to revoke all persistent sessions.

Q: How do you handle distributed session management?

A: Use Redis cluster for session storage, implement sticky sessions with load balancers, or use JWT tokens for stateless authentication. Each approach has trade-offs in terms of complexity and scalability.

External Resources

This comprehensive guide provides a production-ready approach to implementing user login systems with proper authentication, authorization, and session management. The modular design allows for easy maintenance and scaling while maintaining security best practices.