Skip to content

Commit 543c29c

Browse files
authored
Merge pull request #44 from Decodeat/feat/43-logout
feat: 로그아웃시 토큰 삭제
2 parents 7b09149 + 4e74a1b commit 543c29c

File tree

10 files changed

+110
-14
lines changed

10 files changed

+110
-14
lines changed

src/main/java/com/DecodEat/domain/refreshToken/repository/RefreshTokenRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import com.DecodEat.domain.refreshToken.entity.RefreshToken;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
56

67
import java.util.Optional;
78

89
public interface RefreshTokenRepository extends JpaRepository<RefreshToken , Long> {
910
Optional<RefreshToken> findByRefreshToken(String refreshToken);
1011
Optional<RefreshToken> findByUserId(Long userId);
12+
13+
@Modifying
14+
void deleteByUserId(Long userId);
1115
}

src/main/java/com/DecodEat/domain/refreshToken/service/RefreshTokenService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.DecodEat.global.exception.GeneralException;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
89

910
import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.*;
1011

@@ -17,5 +18,8 @@ public RefreshToken findByRefreshToken(String refreshToken){
1718
return refreshTokenRepository.findByRefreshToken(refreshToken)
1819
.orElseThrow(() -> new GeneralException(UNEXPECTED_TOKEN));
1920
}
20-
21+
@Transactional
22+
public void deleteByUserId(Long userId) {
23+
refreshTokenRepository.deleteByUserId(userId);
24+
}
2125
}

src/main/java/com/DecodEat/domain/refreshToken/service/TokenService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public String refreshAccessToken(HttpServletRequest request){
4646
if(!jwtTokenProvider.validToken(refreshToken)){
4747
throw new GeneralException(UNEXPECTED_TOKEN);
4848
}
49-
return createNewAccessToken(refreshToken);
49+
String accessToken = createNewAccessToken(refreshToken);
50+
userService.saveUserAccessToken(userService.findById(refreshTokenService.findByRefreshToken(refreshToken).getUserId()), accessToken);
51+
52+
return accessToken;
5053
}
5154
}

src/main/java/com/DecodEat/domain/users/controller/UserController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import lombok.RequiredArgsConstructor;
1010
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1111
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.PostMapping;
1213
import org.springframework.web.bind.annotation.RestController;
1314

1415
@RequiredArgsConstructor
@@ -20,4 +21,10 @@ public class UserController {
2021
public ApiResponse<UserInfoDto> getMyInfo(@CurrentUser User user) {
2122
return ApiResponse.onSuccess(UserConverter.userToUserInfoDto(user));
2223
}
24+
25+
@PostMapping("/api/logout")
26+
public ApiResponse<String> logout(@CurrentUser User user) {
27+
28+
return ApiResponse.onSuccess("로그아웃.");
29+
}
2330
}

src/main/java/com/DecodEat/domain/users/entity/User.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class User extends BaseEntity {
2424
@Column(nullable = false)
2525
private String nickname;
2626

27+
@Column(length = 1024)
28+
private String accessToken;
29+
2730
// @Column(nullable = false)
2831
// private String password;
2932

@@ -34,4 +37,12 @@ public class User extends BaseEntity {
3437
public void update(String name) {
3538
this.nickname = name;
3639
}
40+
41+
public void updateAccessToken(String accessToken) {
42+
this.accessToken = accessToken;
43+
}
44+
45+
public void expireAccessToken() {
46+
this.accessToken = null;
47+
}
3748
}

src/main/java/com/DecodEat/domain/users/service/UserService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.DecodEat.global.exception.GeneralException;
77
import lombok.RequiredArgsConstructor;
88
import org.springframework.stereotype.Service;
9+
import org.springframework.transaction.annotation.Transactional;
910

1011
@RequiredArgsConstructor
1112
@Service
@@ -22,4 +23,16 @@ public User findByEmail(String email){
2223
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_EXISTED));
2324
}
2425

26+
@Transactional
27+
public void saveUserAccessToken(User user, String accessToken) {
28+
user.updateAccessToken(accessToken);
29+
userRepository.save(user);
30+
}
31+
32+
@Transactional
33+
public void expireAccessToken(User user) {
34+
user.expireAccessToken();
35+
userRepository.save(user);
36+
}
37+
2538
}

src/main/java/com/DecodEat/global/config/TokenAuthenticationFilter.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.DecodEat.global.config;
22

3+
import com.DecodEat.domain.users.entity.User;
4+
import com.DecodEat.domain.users.service.UserService;
35
import com.DecodEat.global.config.jwt.JwtTokenProvider;
46
import jakarta.servlet.FilterChain;
57
import jakarta.servlet.ServletException;
6-
import jakarta.servlet.ServletRequest;
7-
import jakarta.servlet.ServletResponse;
88
import jakarta.servlet.http.HttpServletRequest;
99
import jakarta.servlet.http.HttpServletResponse;
1010
import lombok.RequiredArgsConstructor;
@@ -15,27 +15,32 @@
1515
import java.io.IOException;
1616

1717
@RequiredArgsConstructor
18-
public class TokenAuthenticationFilter extends OncePerRequestFilter { // OncePerRequestFilter : 매 요청마다 필터 한 번만 실행 보장
18+
public class TokenAuthenticationFilter extends OncePerRequestFilter {
1919
private final JwtTokenProvider jwtTokenProvider;
20+
private final UserService userService;
2021
private final static String HEADER_AUTHORIZATION = "Authorization";
2122
private final static String TOKEN_PREFIX = "Bearer ";
2223

2324
@Override
2425
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
25-
// 요청 헤더에서 인증 토큰 가져오기
2626
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
2727
String token = getAccessToken(authorizationHeader);
28-
// 토큰 유효성 확인, 성공시 인증 정보 설정
29-
if(token!=null && jwtTokenProvider.validToken(token)){
30-
Authentication authentication = jwtTokenProvider.getAuthentication(token);
31-
SecurityContextHolder.getContext().setAuthentication(authentication);
28+
29+
if (token != null && jwtTokenProvider.validToken(token)) {
30+
Long userId = jwtTokenProvider.getUserId(token);
31+
User user = userService.findById(userId);
32+
33+
// 저장된 토큰과 일치하는지 확인
34+
if (user.getAccessToken() != null && user.getAccessToken().equals(token)) {
35+
Authentication authentication = jwtTokenProvider.getAuthentication(token);
36+
SecurityContextHolder.getContext().setAuthentication(authentication);
37+
}
3238
}
33-
// 다음 필터로 요청 전달
3439
filterChain.doFilter(request, response);
3540
}
3641

37-
private String getAccessToken(String authorizationHeader){
38-
if(authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)){
42+
private String getAccessToken(String authorizationHeader) {
43+
if (authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)) {
3944
return null;
4045
}
4146
return authorizationHeader.substring(TOKEN_PREFIX.length());
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.DecodEat.global.config;
2+
3+
import com.DecodEat.domain.refreshToken.service.RefreshTokenService;
4+
import com.DecodEat.domain.users.entity.User;
5+
import com.DecodEat.domain.users.service.UserService;
6+
import com.DecodEat.global.config.jwt.JwtTokenProvider;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.security.web.authentication.logout.LogoutHandler;
12+
import org.springframework.stereotype.Component;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class TokenLogoutHandler implements LogoutHandler {
17+
18+
private final JwtTokenProvider jwtTokenProvider;
19+
private final RefreshTokenService refreshTokenService;
20+
private final UserService userService; // 주입
21+
private final static String HEADER_AUTHORIZATION = "Authorization";
22+
private final static String TOKEN_PREFIX = "Bearer ";
23+
24+
@Override
25+
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
26+
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
27+
String accessToken = getAccessToken(authorizationHeader);
28+
29+
if (accessToken != null && jwtTokenProvider.validToken(accessToken)) {
30+
// 1. 리프레시 토큰 삭제
31+
Long userId = jwtTokenProvider.getUserId(accessToken);
32+
refreshTokenService.deleteByUserId(userId);
33+
34+
// 2. User의 accessToken 필드 만료
35+
User user = userService.findById(userId);
36+
userService.expireAccessToken(user);
37+
}
38+
}
39+
40+
private String getAccessToken(String authorizationHeader){
41+
if(authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)){
42+
return null;
43+
}
44+
return authorizationHeader.substring(TOKEN_PREFIX.length());
45+
}
46+
}

src/main/java/com/DecodEat/global/config/WebOAuthSecurityConfig.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class WebOAuthSecurityConfig {
2929
private final RefreshTokenRepository refreshTokenRepository;
3030
private final UserService userService;
3131
private final CorsConfigurationSource corsConfigurationSource; // CorsCongifuragtinoSource Bean 주입 위함
32+
private final TokenLogoutHandler tokenLogoutHandler;
3233
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
3334
private String kakaoClientId;
3435

@@ -80,6 +81,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
8081
// 7. 로그아웃
8182
http.logout(logout -> logout
8283
.logoutUrl("/api/logout")
84+
.addLogoutHandler(tokenLogoutHandler)
8385
// 👇 카카오 로그아웃 URL로 리다이렉트
8486
.logoutSuccessUrl("https://kauth.kakao.com/oauth/logout?client_id=" + kakaoClientId + "&logout_redirect_uri=https://decodeat.store/")
8587
.invalidateHttpSession(true)
@@ -103,7 +105,7 @@ public OAuth2SuccessHandler oAuth2SuccessHandler() {
103105

104106
@Bean
105107
public TokenAuthenticationFilter tokenAuthenticationFilter() {
106-
return new TokenAuthenticationFilter(tokenProvider);
108+
return new TokenAuthenticationFilter(tokenProvider, userService);
107109
}
108110

109111
@Bean

src/main/java/com/DecodEat/global/config/oauth/OAuth2SuccessHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
5353

5454
// 2. 액세스 토큰 생성
5555
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
56+
userService.saveUserAccessToken(user, accessToken); // 액세스 토큰 저장
5657

5758
String targetUrl = getTargetUrl(accessToken);
5859

0 commit comments

Comments
 (0)