diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/io/github/petty/config/SecurityConfig.java b/src/main/java/io/github/petty/config/SecurityConfig.java index 18dc2d7..1ddb6a0 100644 --- a/src/main/java/io/github/petty/config/SecurityConfig.java +++ b/src/main/java/io/github/petty/config/SecurityConfig.java @@ -5,6 +5,8 @@ import io.github.petty.users.jwt.LoginFilter; import io.github.petty.users.oauth2.CustomOAuth2UserService; import io.github.petty.users.oauth2.OAuth2SuccessHandler; +import io.github.petty.users.repository.UsersRepository; +import io.github.petty.users.service.RefreshTokenService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -24,16 +26,22 @@ public class SecurityConfig { private final JWTUtil jwtUtil; private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final RefreshTokenService refreshTokenService; + private final UsersRepository usersRepository; public SecurityConfig( AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, CustomOAuth2UserService customOAuth2UserService, - OAuth2SuccessHandler oAuth2SuccessHandler) { + OAuth2SuccessHandler oAuth2SuccessHandler, + RefreshTokenService refreshTokenService, + UsersRepository usersRepository) { this.authenticationConfiguration = authenticationConfiguration; this.jwtUtil = jwtUtil; this.customOAuth2UserService = customOAuth2UserService; this.oAuth2SuccessHandler = oAuth2SuccessHandler; + this.refreshTokenService = refreshTokenService; + this.usersRepository = usersRepository; } @Bean @@ -55,7 +63,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests((auth) -> auth .requestMatchers("/admin").hasRole("ADMIN") .requestMatchers("/user").authenticated() - .requestMatchers("/login/**", "/oauth2/**").permitAll() + .requestMatchers("/login/**", "/oauth2/**", "/api/auth/refresh").permitAll() .anyRequest().permitAll()) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") @@ -67,12 +75,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .logout(logout -> logout .logoutUrl("/logout") // 클라이언트 측 로그아웃 요청 URL과 일치 .logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트 URL - .deleteCookies("jwt") // 로그아웃 시 jwt 쿠키 삭제 + .deleteCookies("JSESSIONID", "jwt", "refresh_token") // 로그아웃 시 jwt 쿠키 삭제 .invalidateHttpSession(true) .clearAuthentication(true) ) .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class) - .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class) + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenService, usersRepository), UsernamePasswordAuthenticationFilter.class) .sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); diff --git a/src/main/java/io/github/petty/users/controller/UsersApiController.java b/src/main/java/io/github/petty/users/controller/UsersApiController.java index 773d322..ab736ed 100644 --- a/src/main/java/io/github/petty/users/controller/UsersApiController.java +++ b/src/main/java/io/github/petty/users/controller/UsersApiController.java @@ -1,9 +1,15 @@ package io.github.petty.users.controller; import io.github.petty.users.dto.EmailVerificationRequest; +import io.github.petty.users.dto.RefreshTokenResponseDTO; import io.github.petty.users.dto.VerifyCodeRequest; import io.github.petty.users.jwt.JWTUtil; import io.github.petty.users.service.EmailService; +import io.github.petty.users.service.RefreshTokenService; +import io.github.petty.users.util.CookieUtils; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -13,6 +19,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.UUID; @RestController @RequestMapping("/api") @@ -20,10 +27,13 @@ public class UsersApiController { private final JWTUtil jwtUtil; private final EmailService emailService; + private final RefreshTokenService refreshTokenService; - public UsersApiController(JWTUtil jwtUtil, EmailService emailService) { + + public UsersApiController(JWTUtil jwtUtil, EmailService emailService, RefreshTokenService refreshTokenService) { this.jwtUtil = jwtUtil; this.emailService = emailService; + this.refreshTokenService = refreshTokenService; } @GetMapping("/users/me") @@ -62,4 +72,34 @@ public ResponseEntity> verifyCode(@RequestBody VerifyCodeReq return ResponseEntity.ok(response); } + + // 리프레시 토큰 엔드포인트 추가 + @PostMapping("/auth/refresh") + public ResponseEntity refreshToken( + @CookieValue(value = "refresh_token", required = false) String refreshToken, + HttpServletResponse response) { + + if (refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(Map.of("error", "리프레시 토큰이 없습니다.")); + } + + try { + UUID refreshTokenId = UUID.fromString(refreshToken); + RefreshTokenResponseDTO tokenResponse = refreshTokenService.refreshAccessToken(refreshTokenId); + + // 쿠키 설정 코드 + CookieUtils.setTokenCookies(response, + tokenResponse.getAccessToken(), + UUID.fromString(tokenResponse.getRefreshToken())); + + return ResponseEntity.ok(Map.of("message", "토큰이 갱신되었습니다.")); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "유효하지 않은 토큰 형식입니다.")); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(Map.of("error", e.getMessage())); + } + } } \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java b/src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java new file mode 100644 index 0000000..78a3a7d --- /dev/null +++ b/src/main/java/io/github/petty/users/dto/RefreshTokenResponseDTO.java @@ -0,0 +1,13 @@ +package io.github.petty.users.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RefreshTokenResponseDTO { + private String accessToken; + private String refreshToken; +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/entity/RefreshToken.java b/src/main/java/io/github/petty/users/entity/RefreshToken.java new file mode 100644 index 0000000..e34c6d3 --- /dev/null +++ b/src/main/java/io/github/petty/users/entity/RefreshToken.java @@ -0,0 +1,49 @@ +package io.github.petty.users.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private Users user; + + @Column(nullable = false) + private String provider; // 인증 제공자 (local, github 등) + + @CreationTimestamp + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime expiredAt; + + @Column(nullable = false) + private boolean used; + + // 토큰을 사용됨으로 표시 + public void markUsed() { + this.used = true; + } + + // 토큰이 만료되었는지 확인 + public boolean isExpired() { + return LocalDateTime.now().isAfter(this.expiredAt); + } + +} diff --git a/src/main/java/io/github/petty/users/jwt/JWTFilter.java b/src/main/java/io/github/petty/users/jwt/JWTFilter.java index ac4a2ae..6f3b4bb 100644 --- a/src/main/java/io/github/petty/users/jwt/JWTFilter.java +++ b/src/main/java/io/github/petty/users/jwt/JWTFilter.java @@ -28,6 +28,13 @@ public JWTFilter(JWTUtil jwtUtil) { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + // 리프레시 엔드포인트는 JWTFilter 검증 Skip + if (request.getRequestURI().equals("/api/auth/refresh")) { + filterChain.doFilter(request, response); // 바로 다음 필터/컨트롤러로 + return; + } + String token = null; // 1. 쿠키에서 토큰 확인 diff --git a/src/main/java/io/github/petty/users/jwt/JWTUtil.java b/src/main/java/io/github/petty/users/jwt/JWTUtil.java index 4f9f75a..33a2248 100644 --- a/src/main/java/io/github/petty/users/jwt/JWTUtil.java +++ b/src/main/java/io/github/petty/users/jwt/JWTUtil.java @@ -2,6 +2,8 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; @@ -50,4 +52,15 @@ public String createJwt(String username, String role, Long expiredMs) { .signWith(secretKey) .compact(); } + + public String extractRefreshToken(HttpServletRequest request) { + if (request.getCookies() != null) { + for (Cookie cookie : request.getCookies()) { + if ("refresh_token".equals(cookie.getName())) { + return cookie.getValue(); // UUID 문자열을 쿠키에서 가져옴 + } + } + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/jwt/LoginFilter.java b/src/main/java/io/github/petty/users/jwt/LoginFilter.java index 5cf0329..be1f2f4 100644 --- a/src/main/java/io/github/petty/users/jwt/LoginFilter.java +++ b/src/main/java/io/github/petty/users/jwt/LoginFilter.java @@ -3,6 +3,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.github.petty.users.dto.CustomUserDetails; import io.github.petty.users.dto.LoginDTO; +import io.github.petty.users.entity.Users; +import io.github.petty.users.repository.UsersRepository; +import io.github.petty.users.service.RefreshTokenService; +import io.github.petty.users.util.CookieUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -19,20 +23,23 @@ import java.io.IOException; import java.util.Collection; import java.util.Iterator; +import java.util.UUID; public class LoginFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; private final JWTUtil jwtUtil; + private final RefreshTokenService refreshTokenService; + private final UsersRepository usersRepository; // expirationTime 주입 @Value("${jwt.expiration-time}") private long expirationTime; - public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) { - + public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshTokenService refreshTokenService, UsersRepository usersRepository) { this.authenticationManager = authenticationManager; this.jwtUtil = jwtUtil; - + this.refreshTokenService = refreshTokenService; + this.usersRepository = usersRepository; } @Override @@ -61,26 +68,24 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); - String username = customUserDetails.getUsername(); Collection authorities = authentication.getAuthorities(); Iterator iterator = authorities.iterator(); GrantedAuthority auth = iterator.next(); - String role = auth.getAuthority(); + + // 액세스 토큰 생성 String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime - // JWT 토큰을 쿠키에 저장 - Cookie jwtCookie = new Cookie("jwt", token); - jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) - jwtCookie.setPath("/"); // 쿠키의 유효 경로 - // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) - int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 - jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 - response.addCookie(jwtCookie); + // 사용자 조회 + Users user = usersRepository.findByUsername(username); + + // 리프레시 토큰 생성 + UUID refreshToken = refreshTokenService.createRefreshToken(user); -// response.addHeader("Authorization", "Bearer " + token); + // 쿠키 설정 코드 + CookieUtils.setTokenCookies(response, token, refreshToken); } //로그인 실패시 diff --git a/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java b/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java index bfd48cf..b0c7f38 100644 --- a/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java +++ b/src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java @@ -1,6 +1,10 @@ package io.github.petty.users.oauth2; +import io.github.petty.users.entity.Users; import io.github.petty.users.jwt.JWTUtil; +import io.github.petty.users.repository.UsersRepository; +import io.github.petty.users.service.RefreshTokenService; +import io.github.petty.users.util.CookieUtils; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -11,12 +15,15 @@ import org.springframework.stereotype.Component; import java.io.IOException; +import java.util.UUID; @Component @RequiredArgsConstructor public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JWTUtil jwtUtil; + private final RefreshTokenService refreshTokenService; + private final UsersRepository usersRepository; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, @@ -24,20 +31,21 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal(); - // JWT 발급 - String token = jwtUtil.createJwt(oAuth2User.getEmail(), oAuth2User.getAuthorities().iterator().next().getAuthority(), 3600000L); + // 액세스 토큰 생성 + String token = jwtUtil.createJwt(oAuth2User.getEmail(), + oAuth2User.getAuthorities().iterator().next().getAuthority(), + 3600000L); // 1시간 - // JWT 토큰을 쿠키에 저장 - Cookie jwtCookie = new Cookie("jwt", token); - jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) - jwtCookie.setPath("/"); // 쿠키의 유효 경로 - // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) - int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환 - jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정 - response.addCookie(jwtCookie); + // 사용자 조회 + Users user = usersRepository.findByUsername(oAuth2User.getEmail()); - String targetUrl = "/"; + // 리프레시 토큰 생성 + UUID refreshToken = refreshTokenService.createRefreshToken(user); + + // 쿠키 설정 코드 + CookieUtils.setTokenCookies(response, token, refreshToken); + String targetUrl = "/"; getRedirectStrategy().sendRedirect(request, response, targetUrl); } } \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java b/src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..30710fe --- /dev/null +++ b/src/main/java/io/github/petty/users/repository/RefreshTokenRepository.java @@ -0,0 +1,29 @@ +package io.github.petty.users.repository; + +import io.github.petty.users.entity.RefreshToken; +import io.github.petty.users.entity.Users; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface RefreshTokenRepository extends JpaRepository { + + // 특정 사용자의 활성화된 리프레시 토큰 조회 + @Query("SELECT r FROM RefreshToken r WHERE r.user.provider = :provider AND r.user.username = :username AND r.used = false ORDER BY r.createdAt DESC") + List findActiveTokensByProviderAndUsername(@Param("provider") String provider, @Param("username") String username); + + // 사용자의 유효한 활성 토큰 조회 + @Query("SELECT r FROM RefreshToken r WHERE r.user = :user AND r.used = false AND r.expiredAt > :now ORDER BY r.createdAt ASC") + List findActiveTokensByUser(@Param("user") Users user, @Param("now") LocalDateTime now); + + // ID로 사용되지 않은 토큰 조회 + Optional findByIdAndUsedIsFalse(UUID id); +} diff --git a/src/main/java/io/github/petty/users/service/RefreshTokenService.java b/src/main/java/io/github/petty/users/service/RefreshTokenService.java new file mode 100644 index 0000000..ea167c8 --- /dev/null +++ b/src/main/java/io/github/petty/users/service/RefreshTokenService.java @@ -0,0 +1,99 @@ +package io.github.petty.users.service; + +import io.github.petty.users.dto.RefreshTokenResponseDTO; +import io.github.petty.users.entity.RefreshToken; +import io.github.petty.users.entity.Users; +import io.github.petty.users.jwt.JWTUtil; +import io.github.petty.users.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private static final Logger logger = LoggerFactory.getLogger(RefreshTokenService.class); + private final RefreshTokenRepository refreshTokenRepository; + private final JWTUtil jwtUtil; + + // 새 리프레시 토큰 생성 + @Transactional + public UUID createRefreshToken(Users user) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiresAt = now.plusDays(7); // 리프레시 토큰 7일 유효 + + // 활성 토큰 관리 (최대 3개로 제한) + List activeTokens = refreshTokenRepository + .findActiveTokensByUser(user, now); + + final int MAX_TOKENS_PER_USER = 3; + if (activeTokens.size() >= MAX_TOKENS_PER_USER) { + // 가장 오래된 토큰부터 삭제 + for (int i = 0; i < activeTokens.size() - (MAX_TOKENS_PER_USER - 1); i++) { + refreshTokenRepository.delete(activeTokens.get(i)); + } + } + + // 새 리프레시 토큰 생성 + RefreshToken refreshToken = RefreshToken.builder() + .user(user) + .provider(user.getProvider()) + .expiredAt(expiresAt) + .used(false) + .build(); + + RefreshToken savedToken = refreshTokenRepository.save(refreshToken); + logger.debug("리프레시 토큰 생성: 사용자={}, 토큰={}", user.getUsername(), savedToken.getId()); + return savedToken.getId(); + } + + // 액세스 토큰 및 리프레시 토큰 갱신 + @Transactional + public RefreshTokenResponseDTO refreshAccessToken(UUID refreshTokenId) { + RefreshToken refreshToken = refreshTokenRepository.findByIdAndUsedIsFalse(refreshTokenId) + .orElseThrow(() -> new RuntimeException("유효하지 않은 리프레시 토큰입니다.")); + + // 토큰 만료 체크 + if (refreshToken.isExpired()) { + List userTokens = refreshTokenRepository.findActiveTokensByUser( + refreshToken.getUser(), LocalDateTime.now()); + refreshTokenRepository.deleteAll(userTokens); + + logger.warn("만료된 리프레시 토큰: 사용자={}", refreshToken.getUser().getUsername()); + throw new RuntimeException("만료된 리프레시 토큰입니다."); + } + + // 토큰 사용 처리 + refreshToken.markUsed(); + refreshTokenRepository.save(refreshToken); // 즉시 저장 + + // 새 액세스 토큰 생성 + Users user = refreshToken.getUser(); + String newAccessToken = jwtUtil.createJwt( + user.getUsername(), + user.getRole(), + 3600000L); // 1시간 + + // 새 리프레시 토큰 생성 + UUID newRefreshToken = createRefreshToken(user); + + logger.debug("토큰 리프레시 성공: 사용자={}", user.getUsername()); + return new RefreshTokenResponseDTO(newAccessToken, newRefreshToken.toString()); + } + + // 사용자의 모든 리프레시 토큰 무효화 + @Transactional + public void invalidateUserTokens(Users user) { + List activeTokens = refreshTokenRepository.findActiveTokensByUser( + user, LocalDateTime.now()); + refreshTokenRepository.deleteAll(activeTokens); + + logger.debug("사용자 토큰 전체 무효화: 사용자={}", user.getUsername()); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/petty/users/util/CookieUtils.java b/src/main/java/io/github/petty/users/util/CookieUtils.java new file mode 100644 index 0000000..0b60901 --- /dev/null +++ b/src/main/java/io/github/petty/users/util/CookieUtils.java @@ -0,0 +1,31 @@ +package io.github.petty.users.util; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.UUID; + +public class CookieUtils { + // 인스턴스 생성 방지 + private CookieUtils() { + throw new IllegalStateException("Utility class"); + } + + // 액세스 토큰과 리프레시 토큰 쿠키를 한번에 설정 + public static void setTokenCookies(HttpServletResponse response, String accessToken, UUID refreshToken) { + // 액세스 토큰을 쿠키에 저장 + Cookie jwtCookie = new Cookie("jwt", accessToken); + jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어) + jwtCookie.setPath("/"); // 쿠키의 유효 경로 + // jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략) + jwtCookie.setMaxAge(3600);// 쿠키의 만료 시간 설정 (1시간) + response.addCookie(jwtCookie); + + // 리프레시 토큰을 쿠키에 저장 + Cookie refreshCookie = new Cookie("refresh_token", refreshToken.toString()); + refreshCookie.setHttpOnly(true); + refreshCookie.setPath("/"); + refreshCookie.setMaxAge(7 * 24 * 60 * 60); // 7일 + response.addCookie(refreshCookie); + } +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d168429..3974679 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -82,21 +82,43 @@

사용자 정보

await checkLoginStatus(); }); + // RTR 추가 async function checkLoginStatus() { try { - const response = await fetch('/api/users/me'); // 쿠키는 자동으로 전송 + const response = await fetch('/api/users/me'); + if (response.ok) { + // 성공: 로그인 상태 표시 const data = await response.json(); document.getElementById('userMenu').style.display = 'inline'; document.getElementById('loginMenu').style.display = 'none'; document.getElementById('userInfo').style.display = 'block'; document.getElementById('username').textContent = data.username; document.getElementById('role').textContent = data.role; + } else if (response.status === 401) { + // 엑세스 토큰 만료: 자동 갱신 시도 + console.log('액세스 토큰 만료, 리프레시 토큰으로 갱신 시도'); + + try { + const refreshResponse = await fetch('/api/auth/refresh', { + method: 'POST' + }); + + if (refreshResponse.ok) { + console.log('토큰 갱신 성공! 원래 요청 재시도'); + // 토큰 갱신 성공 → 원래 요청 재시도 + return await checkLoginStatus(); + } else { + console.log('리프레시 토큰도 만료, 로그인 필요'); + showLoginMenu(); + } + } catch (refreshError) { + console.error('토큰 갱신 실패:', refreshError); + showLoginMenu(); + } } else { + // 다른 에러 showLoginMenu(); - if (response.status === 401) { - // 쿠키가 만료되었거나 유효하지 않은 경우 - } } } catch (error) { console.error('사용자 정보 조회 실패:', error); diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 5ae3b8d..fe48d6d 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -126,12 +126,8 @@

로그인

}); if (response.ok) { - const token = response.headers.get('Authorization'); - if (token) { - const jwtToken = token.startsWith('Bearer ') ? token.slice(7) : token; - localStorage.setItem('jwt', jwtToken); window.location.href = '/'; - } + // } } else { alert('로그인 실패'); }