Skip to content

Commit aee4a63

Browse files
committed
feat: Implement JWT-based logout with Spring Security
- Add CustomLogoutHandler for cleanup operations - Delete Redis userCache on logout - Publish UserLogoutEvent for analytics - Configure Spring Security logout - Delete JWT cookies (accessToken, refreshToken) - Logout URL: /logout - Redirect to homepage after logout - Add logout() function to common.js - Add UserLogoutEvent domain event
1 parent eb17985 commit aee4a63

4 files changed

Lines changed: 108 additions & 1 deletion

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.axon.core_service.config.auth;
2+
3+
import com.axon.core_service.domain.user.CustomOAuth2User;
4+
import com.axon.core_service.event.UserLogoutEvent;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.context.ApplicationEventPublisher;
10+
import org.springframework.data.redis.core.RedisTemplate;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.web.authentication.logout.LogoutHandler;
13+
import org.springframework.stereotype.Component;
14+
15+
import java.time.Instant;
16+
17+
/**
18+
* Custom logout handler for JWT-based authentication.
19+
* Handles cleanup tasks during logout:
20+
* - Deletes user cache from Redis
21+
* - Publishes logout event for analytics
22+
*/
23+
@Slf4j
24+
@Component
25+
@RequiredArgsConstructor
26+
public class CustomLogoutHandler implements LogoutHandler {
27+
28+
private final RedisTemplate<String, Object> redisTemplate;
29+
private final ApplicationEventPublisher eventPublisher;
30+
31+
/**
32+
* Performs logout cleanup operations when a user logs out.
33+
*
34+
* @param request the HTTP request
35+
* @param response the HTTP response
36+
* @param authentication the current authentication object (may be null if not authenticated)
37+
*/
38+
@Override
39+
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
40+
if (authentication == null) {
41+
log.debug("Logout called with null authentication, skipping cleanup");
42+
return;
43+
}
44+
45+
if (!(authentication.getPrincipal() instanceof CustomOAuth2User)) {
46+
log.debug("Logout called with non-CustomOAuth2User principal, skipping cleanup");
47+
return;
48+
}
49+
50+
CustomOAuth2User customUser = (CustomOAuth2User) authentication.getPrincipal();
51+
Long userId = customUser.getUserId();
52+
53+
try {
54+
// Delete user cache from Redis
55+
String cacheKey = "userCache:" + userId;
56+
Boolean deleted = redisTemplate.delete(cacheKey);
57+
log.info("Logout: userId={}, userCache deleted={}", userId, deleted);
58+
59+
// Publish logout event for analytics
60+
eventPublisher.publishEvent(new UserLogoutEvent(userId, Instant.now()));
61+
log.info("Logout: userId={}, logout event published", userId);
62+
63+
} catch (Exception e) {
64+
log.error("Error during logout cleanup for userId={}", userId, e);
65+
// Don't throw exception - allow logout to proceed even if cleanup fails
66+
}
67+
}
68+
}

core-service/src/main/java/com/axon/core_service/config/auth/SecurityConfig.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class SecurityConfig {
2626
private final CustomOAuth2UserService customOAuth2UserService;
2727
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
2828
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
29+
private final CustomLogoutHandler customLogoutHandler;
2930

3031
/**
3132
* Creates a JwtAuthenticationFilter initialized with the configured
@@ -78,7 +79,12 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilte
7879
new HttpStatusEntryPoint(HttpStatus.OK), // 200 OK 반환 (리다이렉트 방지)
7980
antMatcher("/test/**"))
8081
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/naver")))
81-
.logout(logout -> logout.logoutSuccessUrl("/"))
82+
.logout(logout -> logout
83+
.logoutUrl("/logout")
84+
.logoutSuccessUrl("/")
85+
.deleteCookies("accessToken", "refreshToken") // Delete JWT cookies
86+
.addLogoutHandler(customLogoutHandler) // Custom cleanup (Redis, events)
87+
.permitAll())
8288
.oauth2Login(oauth2 -> oauth2
8389
.loginPage("/oauth2/authorization/naver")
8490
.userInfoEndpoint(userInfo -> userInfo
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.axon.core_service.event;
2+
3+
import lombok.Getter;
4+
import org.springframework.context.ApplicationEvent;
5+
6+
import java.time.Instant;
7+
8+
/**
9+
* Domain event published when a user logs out.
10+
* Used for analytics and auditing purposes.
11+
*/
12+
@Getter
13+
public class UserLogoutEvent extends ApplicationEvent {
14+
private final Long userId;
15+
private final Instant logoutAt;
16+
17+
public UserLogoutEvent(Long userId, Instant logoutAt) {
18+
super(userId);
19+
this.userId = userId;
20+
this.logoutAt = logoutAt;
21+
}
22+
}

core-service/src/main/resources/static/js/common.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ const common = (() => {
1515
return {
1616
getCookie,
1717

18+
/**
19+
* Logout the current user by redirecting to Spring Security logout endpoint.
20+
* This will trigger CustomLogoutHandler which:
21+
* - Deletes user cache from Redis
22+
* - Publishes logout event
23+
* - Clears JWT cookies
24+
*/
25+
logout: function() {
26+
window.location.href = '/logout';
27+
},
28+
1829
/**
1930
* Format filter condition to human-readable string.
2031
* @param {Object} filter - Filter object {type, operator, values}

0 commit comments

Comments
 (0)