Skip to content

Commit 43a52ad

Browse files
authored
Merge pull request #41 from trans-talk/develop
[release] manage blackList token
2 parents 08d3200 + 7a52570 commit 43a52ad

File tree

8 files changed

+147
-85
lines changed

8 files changed

+147
-85
lines changed

src/main/java/com/wootech/transtalk/client/GoogleClient.java

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ public URI buildAuthorizeApiUri() {
5151
.queryParam("response_type", "code")
5252
.queryParam("scope", "email profile")
5353
.queryParam("access_type", "offline")
54+
.queryParam("prompt", "select_account")
5455
.build()
5556
.toUri();
5657
}
5758

5859
public String requestToken(String code) {
59-
URI uri = UriComponentsBuilder.fromUriString(this.tokenUri)
60-
.build()
61-
.toUri();
62-
log.info("[GoogleClient] API URI: {}", uri);
60+
URI uri = UriComponentsBuilder.fromUriString(this.tokenUri)
61+
.build()
62+
.toUri();
63+
log.info("[GoogleClient] API URI: {}", uri);
6364

6465
GoogleApiRequest tokenRequest = GoogleApiRequest.builder()
6566
.code(code)
@@ -69,26 +70,25 @@ public String requestToken(String code) {
6970
.grantType("authorization_code")
7071
.build();
7172

72-
GoogleApiResponse tokenResponse = restClient.post()
73-
.uri(uri)
74-
.header(HttpHeaders.ACCEPT)
75-
.acceptCharset(StandardCharsets.UTF_8)
76-
.contentType(MediaType.APPLICATION_JSON)
77-
.body(tokenRequest)
78-
.retrieve()
79-
.onStatus(HttpStatusCode::isError, ((request, response) -> {
80-
String body = new String(response.getBody().readAllBytes());
81-
log.error("[GoogleAPI] Raw Response: {}", body);
82-
83-
throw new GoogleApiException("Token Request Failed: " + body, response.getStatusCode());
84-
}))
85-
.body(GoogleApiResponse.class);
86-
87-
// TODO: external api logging
88-
log.info("[GoogleAPI] Token Response: {}", tokenResponse);
89-
90-
String accessToken = extractAccessCode(tokenResponse);
91-
return accessToken;
73+
GoogleApiResponse tokenResponse = restClient.post()
74+
.uri(uri)
75+
.header(HttpHeaders.ACCEPT)
76+
.acceptCharset(StandardCharsets.UTF_8)
77+
.contentType(MediaType.APPLICATION_JSON)
78+
.body(tokenRequest)
79+
.retrieve()
80+
.onStatus(HttpStatusCode::isError, ((request, response) -> {
81+
String body = new String(response.getBody().readAllBytes());
82+
log.error("[GoogleAPI] Raw Response: {}", body);
83+
84+
throw new GoogleApiException("Token Request Failed: " + body, response.getStatusCode());
85+
}))
86+
.body(GoogleApiResponse.class);
87+
88+
log.info("[GoogleAPI] Token Response: {}", tokenResponse);
89+
90+
String accessToken = extractAccessCode(tokenResponse);
91+
return accessToken;
9292
}
9393

9494
private String extractAccessCode(GoogleApiResponse response) {
@@ -107,7 +107,7 @@ public GoogleProfileResponse requestProfile(String accessToken) {
107107

108108
GoogleProfileResponse googleProfileResponse = restClient.get()
109109
.uri(uri)
110-
.header(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + accessToken)
110+
.header(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + accessToken)
111111
.retrieve()
112112
.onStatus(HttpStatusCode::isError, ((request, response) -> {
113113
String body = new String(response.getBody().readAllBytes());
Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.wootech.transtalk.config;
22

33
import com.wootech.transtalk.config.util.JwtUtil;
4-
import com.wootech.transtalk.dto.auth.AuthUser;
4+
import com.wootech.transtalk.service.auth.BlackListService;
55
import com.wootech.transtalk.service.auth.RefreshTokenService;
66
import jakarta.servlet.http.Cookie;
77
import jakarta.servlet.http.HttpServletRequest;
@@ -12,16 +12,60 @@
1212
import org.springframework.security.web.authentication.logout.LogoutHandler;
1313
import org.springframework.stereotype.Component;
1414

15+
import java.util.Date;
16+
17+
import static com.wootech.transtalk.config.util.CookieUtil.deleteRefreshTokenCookie;
18+
1519
@Slf4j
1620
@Component
1721
@RequiredArgsConstructor
1822
public class AppLogOutHandler implements LogoutHandler {
1923

2024
private final RefreshTokenService refreshTokenService;
2125
private final JwtUtil jwtUtil;
26+
private final BlackListService blackListService;
2227

2328
@Override
2429
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
30+
String accessToken = extractAccessToken(request);
31+
32+
if (accessToken != null && jwtUtil.validateToken(accessToken)) {
33+
String jti = jwtUtil.extractJti(accessToken);
34+
Date exp = jwtUtil.extractExpiration(accessToken);
35+
long ttlSeconds = Math.max(0, (exp.getTime() - System.currentTimeMillis()) / 1000);
36+
if (jti != null && ttlSeconds > 0) {
37+
blackListService.add(jti, ttlSeconds);
38+
log.info("[AppLogOutHandler] Access Token JTI Blacklisted JTI={} ttl={}s", jti, ttlSeconds);
39+
}
40+
41+
// access token 만료시
42+
String refreshToken = extractCookieValue(request);
43+
if (refreshToken != null) {
44+
try {
45+
Long userId = Long.parseLong(jwtUtil.extractUserId(refreshToken));
46+
if (refreshTokenService.hasRefreshToken(userId, refreshToken)) {
47+
refreshTokenService.deleteRefreshToken(userId, refreshToken);
48+
}
49+
} catch (Exception e) {
50+
log.warn("[AppLogOutHandler] Failed to Delete Refresh Token: {}", e.getMessage());
51+
}
52+
}
53+
54+
deleteRefreshTokenCookie(response);
55+
log.info("[LogOutHandler] Refresh Token Set Null");
56+
}
57+
}
58+
59+
60+
private String extractAccessToken(HttpServletRequest request) {
61+
String header = request.getHeader("Authorization");
62+
if (header != null && header.startsWith("Bearer ")) {
63+
return header.substring(7);
64+
}
65+
return null;
66+
}
67+
68+
private String extractCookieValue(HttpServletRequest request) {
2569
// 쿠키에서 refresh token 조회
2670
String refreshToken = null;
2771
Cookie[] cookies = request.getCookies();
@@ -33,41 +77,6 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
3377
}
3478
}
3579
}
36-
37-
Long userId = null;
38-
39-
// access token 유효시
40-
if (authentication != null && authentication.getPrincipal() instanceof AuthUser) {
41-
AuthUser authUser = (AuthUser) authentication.getPrincipal();
42-
userId = authUser.getUserId();
43-
log.info("[AppLogOutHandler] - Get Authentication From User ID={}", userId);
44-
}
45-
46-
// access token 만료시
47-
if (userId == null && refreshToken != null && jwtUtil.validateToken(refreshToken)) {
48-
try {
49-
userId = Long.parseLong(jwtUtil.extractUserId(refreshToken));
50-
log.info("[LogOutHandler] Log Out Requested: Extract User ID={} From Refresh Token", userId);
51-
} catch (Exception e) {
52-
log.warn("Extract Failed From Refresh Token={}", e.getMessage());
53-
}
54-
}
55-
56-
if (userId != null && refreshToken != null) {
57-
if (refreshTokenService.hasRefreshToken(userId, refreshToken)) {
58-
refreshTokenService.deleteRefreshToken(userId, refreshToken);
59-
log.info("[LogOutHandler] Delete Refresh Token Succeed");
60-
} else {
61-
log.warn("[LogOutHandler] Delete Refresh Token Failed Refresh Token={}", refreshToken);
62-
}
63-
64-
Cookie refreshTokenCookie = new Cookie("refreshToken", null);
65-
refreshTokenCookie.setMaxAge(0);
66-
refreshTokenCookie.setPath("/");
67-
refreshTokenCookie.setHttpOnly(true);
68-
refreshTokenCookie.setSecure(true);
69-
response.addCookie(refreshTokenCookie);
70-
log.info("[LogOutHandler] Refresh Token Set Null");
71-
}
80+
return refreshToken;
7281
}
7382
}

src/main/java/com/wootech/transtalk/config/DataInitializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import java.time.Instant;
1717
import java.util.List;
1818

19-
@Component
19+
//@Component
2020
@RequiredArgsConstructor
2121
public class DataInitializer {
2222
private final UserRepository userRepository;

src/main/java/com/wootech/transtalk/config/jwt/JwtAuthenticationFilter.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.wootech.transtalk.config.jwt;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import com.wootech.transtalk.config.util.JwtUtil;
5+
import com.wootech.transtalk.dto.ApiResponse;
46
import com.wootech.transtalk.dto.auth.AuthUser;
57
import com.wootech.transtalk.enums.UserRole;
68
import com.wootech.transtalk.exception.custom.NotFoundException;
9+
import com.wootech.transtalk.service.auth.BlackListService;
710
import com.wootech.transtalk.service.user.UserDetailsServiceImpl;
811
import io.jsonwebtoken.Claims;
912
import io.jsonwebtoken.ExpiredJwtException;
@@ -16,13 +19,17 @@
1619
import lombok.NonNull;
1720
import lombok.RequiredArgsConstructor;
1821
import lombok.extern.slf4j.Slf4j;
22+
import org.springframework.http.HttpStatus;
1923
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
24+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
2025
import org.springframework.security.core.AuthenticationException;
26+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2127
import org.springframework.security.core.context.SecurityContextHolder;
2228
import org.springframework.stereotype.Component;
2329
import org.springframework.web.filter.OncePerRequestFilter;
2430

2531
import java.io.IOException;
32+
import java.util.List;
2633

2734
import static com.wootech.transtalk.exception.ErrorMessages.*;
2835

@@ -33,6 +40,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3340

3441
private final JwtUtil jwtUtil;
3542
private final UserDetailsServiceImpl userDetailsService;
43+
private final BlackListService blackListService;
44+
private final ObjectMapper objectMapper;
3645

3746
@Override
3847
protected void doFilterInternal(
@@ -45,6 +54,16 @@ protected void doFilterInternal(
4554
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
4655
String jwt = jwtUtil.substringToken(authorizationHeader);
4756

57+
String jti = jwtUtil.extractJti(jwt);
58+
if (jti != null && blackListService.contains(jti)) {
59+
// 블랙리스트에 있으면 즉시 인증 실패 처리
60+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
61+
response.setContentType("application/json;charset=UTF-8");
62+
ApiResponse<Void> body = ApiResponse.error(LOGGED_OUT_USER_ERROR, HttpStatus.UNAUTHORIZED.name());
63+
response.getWriter().write(objectMapper.writeValueAsString(body));
64+
return;
65+
}
66+
4867
try {
4968
Claims claims = jwtUtil.extractClaims(jwt);
5069
String email = jwtUtil.getEmail(jwt);

src/main/java/com/wootech/transtalk/config/jwt/JwtChannelInterceptor.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22

33
import com.wootech.transtalk.config.util.JwtUtil;
44
import com.wootech.transtalk.enums.UserRole;
5-
import com.wootech.transtalk.exception.custom.NotFoundException;
6-
import com.wootech.transtalk.exception.custom.UnauthorizedException;
75
import com.wootech.transtalk.event.Events;
86
import com.wootech.transtalk.event.ExitToChatRoomEvent;
7+
import com.wootech.transtalk.exception.custom.NotFoundException;
8+
import com.wootech.transtalk.exception.custom.UnauthorizedException;
9+
import com.wootech.transtalk.service.auth.BlackListService;
910
import com.wootech.transtalk.service.user.UserDetailsServiceImpl;
1011
import lombok.RequiredArgsConstructor;
1112
import lombok.extern.slf4j.Slf4j;
1213
import org.springframework.http.HttpStatusCode;
1314
import org.springframework.messaging.Message;
1415
import org.springframework.messaging.MessageChannel;
16+
import org.springframework.messaging.MessagingException;
1517
import org.springframework.messaging.simp.stomp.StompCommand;
1618
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
1719
import org.springframework.messaging.support.ChannelInterceptor;
@@ -21,8 +23,7 @@
2123

2224
import java.util.List;
2325

24-
import static com.wootech.transtalk.exception.ErrorMessages.JWT_DOES_NOT_EXIST_ERROR;
25-
import static com.wootech.transtalk.exception.ErrorMessages.WITHDRAWN_USER_ERROR;
26+
import static com.wootech.transtalk.exception.ErrorMessages.*;
2627

2728
@Slf4j
2829
@Component
@@ -31,6 +32,7 @@ public class JwtChannelInterceptor implements ChannelInterceptor {
3132

3233
private final JwtUtil jwtUtil;
3334
private final UserDetailsServiceImpl userDetailsService;
35+
private final BlackListService blackListService;
3436

3537
@Override
3638
public Message<?> preSend(Message<?> message, MessageChannel channel) {
@@ -67,8 +69,14 @@ private void handleConnect(StompHeaderAccessor accessor) {
6769
accessor.setUser(new UsernamePasswordAuthenticationToken(userEmail, null, List.of(UserRole.ROLE_USER)));
6870
log.info("[JwtChannelInterceptor] CONNECT - JWT Token validated for user={}", userEmail);
6971
}
72+
7073
private String extractAccessToken(StompHeaderAccessor accessor) {
7174
String accessToken = accessor.getFirstNativeHeader("Authorization");
75+
// 로그아웃 처리
76+
String jti = jwtUtil.extractJti(accessToken);
77+
if (blackListService.contains(jti)) {
78+
throw new MessagingException(LOGGED_OUT_USER_ERROR);
79+
}
7280
if (accessToken == null || accessToken.isBlank()) {
7381
log.error("[JwtChannelInterceptor] {}", JWT_DOES_NOT_EXIST_ERROR);
7482
throw new UnauthorizedException(JWT_DOES_NOT_EXIST_ERROR, HttpStatusCode.valueOf(401));
@@ -78,6 +86,7 @@ private String extractAccessToken(StompHeaderAccessor accessor) {
7886

7987
return accessToken.trim();
8088
}
89+
8190
private void validateAccessToken(String accessToken) {
8291
try {
8392
if (!jwtUtil.validateToken(accessToken)) {
@@ -96,6 +105,7 @@ private void inToChatRoom(StompHeaderAccessor accessor) {
96105

97106
handleConnect(accessor);
98107
}
108+
99109
private void exitToChatRoom(StompHeaderAccessor accessor) {
100110
String subscriptionId = accessor.getSubscriptionId();
101111

@@ -107,7 +117,7 @@ private void exitToChatRoom(StompHeaderAccessor accessor) {
107117
String roomId = destination.replace("/topic/chat/", "");
108118
String userEmail = accessor.getUser().getName();
109119

110-
Events.raise(new ExitToChatRoomEvent(userEmail,Long.valueOf(roomId)));
120+
Events.raise(new ExitToChatRoomEvent(userEmail, Long.valueOf(roomId)));
111121
}
112122

113123
}

src/main/java/com/wootech/transtalk/config/util/JwtUtil.java

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.wootech.transtalk.config.util;
22

33
import com.wootech.transtalk.enums.UserRole;
4-
import com.wootech.transtalk.exception.custom.UnauthorizedException;
54
import com.wootech.transtalk.exception.custom.ExpiredJwtException;
5+
import com.wootech.transtalk.exception.custom.UnauthorizedException;
66
import io.jsonwebtoken.*;
77
import io.jsonwebtoken.security.Keys;
88
import jakarta.annotation.PostConstruct;
@@ -42,6 +42,7 @@ public void init() {
4242

4343
public String createAccessToken(Long userId, String email, String name, UserRole userRole) {
4444
Date date = new Date();
45+
String jti = UUID.randomUUID().toString();
4546

4647
return BEARER_PREFIX +
4748
Jwts.builder()
@@ -50,6 +51,7 @@ public String createAccessToken(Long userId, String email, String name, UserRole
5051
.claim("name", name)
5152
.claim("userRole", userRole)
5253
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_TIME))
54+
.id(jti)
5355
.setIssuedAt(date)
5456
.signWith(key, signatureAlgorithm)
5557
.compact();
@@ -111,22 +113,6 @@ public String getEmail(String token) {
111113
return extractClaims(token).get("email", String.class);
112114
}
113115

114-
// refresh token 유효성 검증
115-
public boolean validateRefreshToken(String token) {
116-
try {
117-
Jwts.parser()
118-
.verifyWith(key)
119-
.build()
120-
.parseSignedClaims(token);
121-
return true;
122-
} catch (ExpiredJwtException e) {
123-
log.error(EXPIRED_REFRESH_TOKEN_ERROR + ": {}", e.getMessage());
124-
throw new ExpiredJwtException(EXPIRED_REFRESH_TOKEN_ERROR, HttpStatus.valueOf(406));
125-
} catch (Exception e) {
126-
return false;
127-
}
128-
}
129-
130116
// refresh token 에서 userId 값 추출
131117
public String extractUserId(String refreshToken) {
132118
Claims claims = Jwts.parser()
@@ -137,4 +123,12 @@ public String extractUserId(String refreshToken) {
137123

138124
return claims.get("userId", String.class);
139125
}
126+
127+
public String extractJti(String token) {
128+
return extractClaims(token).getId();
129+
}
130+
131+
public Date extractExpiration(String token) {
132+
return extractClaims(token).getExpiration();
133+
}
140134
}

0 commit comments

Comments
 (0)