diff --git a/src/main/java/com/DecodEat/domain/refreshToken/repository/RefreshTokenRepository.java b/src/main/java/com/DecodEat/domain/refreshToken/repository/RefreshTokenRepository.java index ddbfe62..e009471 100644 --- a/src/main/java/com/DecodEat/domain/refreshToken/repository/RefreshTokenRepository.java +++ b/src/main/java/com/DecodEat/domain/refreshToken/repository/RefreshTokenRepository.java @@ -2,10 +2,14 @@ import com.DecodEat.domain.refreshToken.entity.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import java.util.Optional; public interface RefreshTokenRepository extends JpaRepository { Optional findByRefreshToken(String refreshToken); Optional findByUserId(Long userId); + + @Modifying + void deleteByUserId(Long userId); } diff --git a/src/main/java/com/DecodEat/domain/refreshToken/service/RefreshTokenService.java b/src/main/java/com/DecodEat/domain/refreshToken/service/RefreshTokenService.java index bf4fbf1..028ace1 100644 --- a/src/main/java/com/DecodEat/domain/refreshToken/service/RefreshTokenService.java +++ b/src/main/java/com/DecodEat/domain/refreshToken/service/RefreshTokenService.java @@ -5,6 +5,7 @@ import com.DecodEat.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.*; @@ -17,5 +18,8 @@ public RefreshToken findByRefreshToken(String refreshToken){ return refreshTokenRepository.findByRefreshToken(refreshToken) .orElseThrow(() -> new GeneralException(UNEXPECTED_TOKEN)); } - + @Transactional + public void deleteByUserId(Long userId) { + refreshTokenRepository.deleteByUserId(userId); + } } diff --git a/src/main/java/com/DecodEat/domain/refreshToken/service/TokenService.java b/src/main/java/com/DecodEat/domain/refreshToken/service/TokenService.java index 2ec3ca1..ff07821 100644 --- a/src/main/java/com/DecodEat/domain/refreshToken/service/TokenService.java +++ b/src/main/java/com/DecodEat/domain/refreshToken/service/TokenService.java @@ -46,6 +46,9 @@ public String refreshAccessToken(HttpServletRequest request){ if(!jwtTokenProvider.validToken(refreshToken)){ throw new GeneralException(UNEXPECTED_TOKEN); } - return createNewAccessToken(refreshToken); + String accessToken = createNewAccessToken(refreshToken); + userService.saveUserAccessToken(userService.findById(refreshTokenService.findByRefreshToken(refreshToken).getUserId()), accessToken); + + return accessToken; } } diff --git a/src/main/java/com/DecodEat/domain/users/controller/UserController.java b/src/main/java/com/DecodEat/domain/users/controller/UserController.java index 64126b5..fc365d6 100644 --- a/src/main/java/com/DecodEat/domain/users/controller/UserController.java +++ b/src/main/java/com/DecodEat/domain/users/controller/UserController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @@ -20,4 +21,10 @@ public class UserController { public ApiResponse getMyInfo(@CurrentUser User user) { return ApiResponse.onSuccess(UserConverter.userToUserInfoDto(user)); } + + @PostMapping("/api/logout") + public ApiResponse logout(@CurrentUser User user) { + + return ApiResponse.onSuccess("로그아웃."); + } } diff --git a/src/main/java/com/DecodEat/domain/users/entity/User.java b/src/main/java/com/DecodEat/domain/users/entity/User.java index d0d7dbb..0329e8d 100644 --- a/src/main/java/com/DecodEat/domain/users/entity/User.java +++ b/src/main/java/com/DecodEat/domain/users/entity/User.java @@ -24,6 +24,9 @@ public class User extends BaseEntity { @Column(nullable = false) private String nickname; + @Column(length = 1024) + private String accessToken; + // @Column(nullable = false) // private String password; @@ -34,4 +37,12 @@ public class User extends BaseEntity { public void update(String name) { this.nickname = name; } + + public void updateAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void expireAccessToken() { + this.accessToken = null; + } } \ No newline at end of file diff --git a/src/main/java/com/DecodEat/domain/users/service/UserService.java b/src/main/java/com/DecodEat/domain/users/service/UserService.java index 120da21..4e20828 100644 --- a/src/main/java/com/DecodEat/domain/users/service/UserService.java +++ b/src/main/java/com/DecodEat/domain/users/service/UserService.java @@ -6,6 +6,7 @@ import com.DecodEat.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -22,4 +23,16 @@ public User findByEmail(String email){ .orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_EXISTED)); } + @Transactional + public void saveUserAccessToken(User user, String accessToken) { + user.updateAccessToken(accessToken); + userRepository.save(user); + } + + @Transactional + public void expireAccessToken(User user) { + user.expireAccessToken(); + userRepository.save(user); + } + } diff --git a/src/main/java/com/DecodEat/global/config/TokenAuthenticationFilter.java b/src/main/java/com/DecodEat/global/config/TokenAuthenticationFilter.java index 3464633..6dc7ba5 100644 --- a/src/main/java/com/DecodEat/global/config/TokenAuthenticationFilter.java +++ b/src/main/java/com/DecodEat/global/config/TokenAuthenticationFilter.java @@ -1,10 +1,10 @@ package com.DecodEat.global.config; +import com.DecodEat.domain.users.entity.User; +import com.DecodEat.domain.users.service.UserService; import com.DecodEat.global.config.jwt.JwtTokenProvider; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -15,27 +15,32 @@ import java.io.IOException; @RequiredArgsConstructor -public class TokenAuthenticationFilter extends OncePerRequestFilter { // OncePerRequestFilter : 매 요청마다 필터 한 번만 실행 보장 +public class TokenAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; private final static String HEADER_AUTHORIZATION = "Authorization"; private final static String TOKEN_PREFIX = "Bearer "; @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - // 요청 헤더에서 인증 토큰 가져오기 String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION); String token = getAccessToken(authorizationHeader); - // 토큰 유효성 확인, 성공시 인증 정보 설정 - if(token!=null && jwtTokenProvider.validToken(token)){ - Authentication authentication = jwtTokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(authentication); + + if (token != null && jwtTokenProvider.validToken(token)) { + Long userId = jwtTokenProvider.getUserId(token); + User user = userService.findById(userId); + + // 저장된 토큰과 일치하는지 확인 + if (user.getAccessToken() != null && user.getAccessToken().equals(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } - // 다음 필터로 요청 전달 filterChain.doFilter(request, response); } - private String getAccessToken(String authorizationHeader){ - if(authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)){ + private String getAccessToken(String authorizationHeader) { + if (authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)) { return null; } return authorizationHeader.substring(TOKEN_PREFIX.length()); diff --git a/src/main/java/com/DecodEat/global/config/TokenLogoutHandler.java b/src/main/java/com/DecodEat/global/config/TokenLogoutHandler.java new file mode 100644 index 0000000..8c7ca45 --- /dev/null +++ b/src/main/java/com/DecodEat/global/config/TokenLogoutHandler.java @@ -0,0 +1,46 @@ +package com.DecodEat.global.config; + +import com.DecodEat.domain.refreshToken.service.RefreshTokenService; +import com.DecodEat.domain.users.entity.User; +import com.DecodEat.domain.users.service.UserService; +import com.DecodEat.global.config.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenLogoutHandler implements LogoutHandler { + + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; + private final UserService userService; // 주입 + private final static String HEADER_AUTHORIZATION = "Authorization"; + private final static String TOKEN_PREFIX = "Bearer "; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION); + String accessToken = getAccessToken(authorizationHeader); + + if (accessToken != null && jwtTokenProvider.validToken(accessToken)) { + // 1. 리프레시 토큰 삭제 + Long userId = jwtTokenProvider.getUserId(accessToken); + refreshTokenService.deleteByUserId(userId); + + // 2. User의 accessToken 필드 만료 + User user = userService.findById(userId); + userService.expireAccessToken(user); + } + } + + private String getAccessToken(String authorizationHeader){ + if(authorizationHeader == null || !authorizationHeader.startsWith(TOKEN_PREFIX)){ + return null; + } + return authorizationHeader.substring(TOKEN_PREFIX.length()); + } +} diff --git a/src/main/java/com/DecodEat/global/config/WebOAuthSecurityConfig.java b/src/main/java/com/DecodEat/global/config/WebOAuthSecurityConfig.java index 5f804e5..f6ba47e 100644 --- a/src/main/java/com/DecodEat/global/config/WebOAuthSecurityConfig.java +++ b/src/main/java/com/DecodEat/global/config/WebOAuthSecurityConfig.java @@ -29,6 +29,7 @@ public class WebOAuthSecurityConfig { private final RefreshTokenRepository refreshTokenRepository; private final UserService userService; private final CorsConfigurationSource corsConfigurationSource; // CorsCongifuragtinoSource Bean 주입 위함 + private final TokenLogoutHandler tokenLogoutHandler; @Value("${spring.security.oauth2.client.registration.kakao.client-id}") private String kakaoClientId; @@ -80,6 +81,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // 7. 로그아웃 http.logout(logout -> logout .logoutUrl("/api/logout") + .addLogoutHandler(tokenLogoutHandler) // 👇 카카오 로그아웃 URL로 리다이렉트 .logoutSuccessUrl("https://kauth.kakao.com/oauth/logout?client_id=" + kakaoClientId + "&logout_redirect_uri=https://decodeat.store/") .invalidateHttpSession(true) @@ -103,7 +105,7 @@ public OAuth2SuccessHandler oAuth2SuccessHandler() { @Bean public TokenAuthenticationFilter tokenAuthenticationFilter() { - return new TokenAuthenticationFilter(tokenProvider); + return new TokenAuthenticationFilter(tokenProvider, userService); } @Bean diff --git a/src/main/java/com/DecodEat/global/config/oauth/OAuth2SuccessHandler.java b/src/main/java/com/DecodEat/global/config/oauth/OAuth2SuccessHandler.java index 7314dd4..7dfdc26 100644 --- a/src/main/java/com/DecodEat/global/config/oauth/OAuth2SuccessHandler.java +++ b/src/main/java/com/DecodEat/global/config/oauth/OAuth2SuccessHandler.java @@ -53,6 +53,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // 2. 액세스 토큰 생성 String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION); + userService.saveUserAccessToken(user, accessToken); // 액세스 토큰 저장 String targetUrl = getTargetUrl(accessToken);