Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefreshToken , Long> {
Optional<RefreshToken> findByRefreshToken(String refreshToken);
Optional<RefreshToken> findByUserId(Long userId);

@Modifying
void deleteByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,4 +21,10 @@ public class UserController {
public ApiResponse<UserInfoDto> getMyInfo(@CurrentUser User user) {
return ApiResponse.onSuccess(UserConverter.userToUserInfoDto(user));
}

@PostMapping("/api/logout")
public ApiResponse<String> logout(@CurrentUser User user) {

return ApiResponse.onSuccess("로그아웃.");
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/DecodEat/domain/users/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/DecodEat/domain/users/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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());
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/DecodEat/global/config/TokenLogoutHandler.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -103,7 +105,7 @@ public OAuth2SuccessHandler oAuth2SuccessHandler() {

@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(tokenProvider);
return new TokenAuthenticationFilter(tokenProvider, userService);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading