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
4 changes: 0 additions & 4 deletions cookies.txt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.back.domain.user.controller;

import com.back.domain.user.dto.RefreshTokenResDto;
import com.back.domain.user.dto.UserMeResDto;
import com.back.domain.user.service.UserAuthService;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -11,6 +12,7 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -50,4 +52,16 @@ public RsData<Void> logout(HttpServletRequest request, HttpServletResponse respo
userAuthService.logout(request, response);
return RsData.of(200, "로그아웃되었습니다.");
}

@Operation(summary = "현재 로그인한 유저 정보 조회", description = "세션 유효성 검증 및 사용자 정보 반환")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "인증된 유저 정보 반환 성공"),
@ApiResponse(responseCode = "401", description = "인증되지 않은 사용자"),
@ApiResponse(responseCode = "500", description = "서버 내부 오류")
})
@GetMapping("/me")
public RsData<UserMeResDto> getCurrentUser() {
UserMeResDto userInfo = userAuthService.getCurrentUser();
return RsData.of(200, "인증된 유저 정보 반환 성공", userInfo);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/back/domain/user/dto/UserMeResDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.back.domain.user.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class UserMeResDto {
@JsonProperty("user")
private final UserInfo user;

@Getter
@Builder
public static class UserInfo {
private final String id;
private final String email;
private final String nickname;

@JsonProperty("is_first_login")
private final Boolean isFirstLogin;

@JsonProperty("abv_degree")
private final Double abvDegree;

private final String provider;
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/back/domain/user/service/UserAuthService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.back.domain.user.service;

import com.back.domain.user.dto.RefreshTokenResDto;
import com.back.domain.user.dto.UserMeResDto;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.ServiceException;
import com.back.global.jwt.JwtUtil;
import com.back.global.jwt.refreshToken.entity.RefreshToken;
import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository;
import com.back.global.jwt.refreshToken.service.RefreshTokenService;
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -76,6 +78,7 @@ public class UserAuthService {
private final UserRepository userRepository;
private final RefreshTokenService refreshTokenService;
private final RefreshTokenRepository refreshTokenRepository;
private final Rq rq;

//OAuth 관련

Expand Down Expand Up @@ -211,4 +214,50 @@ public void setFirstLoginFalse(Long id) {
Optional<User> userOpt = userRepository.findById(id);
userOpt.ifPresent(user -> user.setFirstLogin(false));
}

// 현재 로그인한 사용자 정보 조회 (세션 검증용)
public UserMeResDto getCurrentUser() {
try {
User actor = rq.getActor();

if (actor == null) {
log.debug("인증되지 않은 사용자");
throw new ServiceException(401, "인증되지 않은 사용자");
}

Optional<User> userOpt = userRepository.findById(actor.getId());
if (userOpt.isEmpty()) {
log.warn("사용자 ID {}를 DB에서 찾을 수 없음 (토큰은 유효하나 사용자 삭제됨)", actor.getId());
throw new ServiceException(401, "인증되지 않은 사용자");
}

User user = userOpt.get();
String provider = extractProvider(user.getOauthId());

return UserMeResDto.builder()
.user(UserMeResDto.UserInfo.builder()
.id(user.getId().toString())
.email(user.getEmail())
.nickname(user.getNickname())
.isFirstLogin(user.isFirstLogin())
.abvDegree(user.getAbvDegree())
.provider(provider)
.build())
.build();

} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("사용자 정보 조회 중 서버 오류 발생: {}", e.getMessage(), e);
throw new ServiceException(500, "서버 내부 오류");
}
}

private String extractProvider(String oauthId) {
if (oauthId == null || oauthId.isBlank()) {
return "unknown";
}
String[] parts = oauthId.split("_", 2);
return parts.length > 0 ? parts[0] : "unknown";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "refresh_tokens")
@Getter
@Setter
Expand All @@ -20,24 +23,33 @@ public class RefreshToken {
@Column(nullable = false)
private Long userId;

@Column(nullable = false)
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(nullable = false)
private LocalDateTime expiresAt;

@Column(nullable = false)
private LocalDateTime lastUsedAt;

public static RefreshToken create(String token, Long userId, long ttlSeconds) {
LocalDateTime now = LocalDateTime.now();
return RefreshToken.builder()
.token(token)
.userId(userId)
.createdAt(now)
.lastUsedAt(now)
.expiresAt(now.plusSeconds(ttlSeconds))
.build();
}

public boolean isExpired() {
return LocalDateTime.now().isAfter(this.expiresAt);
}

public boolean isIdleExpired(long idleTimeoutHours) {
return LocalDateTime.now().isAfter(this.lastUsedAt.plusMinutes(idleTimeoutHours));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public class RefreshTokenService {
@Value("${custom.refreshToken.expirationSeconds}")
private long refreshTokenExpiration;

@Value("${custom.refreshToken.idleTimeoutHours}")
private long idleTimeoutHours;

// 기존 리프레시 토큰 삭제하고 생성
@Transactional
public String generateRefreshToken(Long userId) {
Expand All @@ -37,19 +40,32 @@ public String generateRefreshToken(Long userId) {
return token;
}

//검증 (만료 체크 포함)
//검증 (만료 체크 및 Idle Timeout 체크 포함)
@Transactional
public boolean validateToken(String token) {
Optional<RefreshToken> tokenOpt = refreshTokenRepository.findByToken(token);
if (tokenOpt.isEmpty()) {
return false;
}

RefreshToken refreshToken = tokenOpt.get();

// 1. 만료 체크 (30일)
if (refreshToken.isExpired()) {
revokeToken(token); // 만료된 토큰 삭제
return false;
}

// 2. Idle Timeout 체크 (4시간)
if (refreshToken.isIdleExpired(idleTimeoutHours)) {
revokeToken(token); // Idle 초과 토큰 삭제
return false;
}

// 3. lastUsedAt 갱신 (사용 시간 업데이트)
refreshToken.setLastUsedAt(LocalDateTime.now());
refreshTokenRepository.save(refreshToken);

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,32 +110,8 @@ private void work(HttpServletRequest request, HttpServletResponse response, Filt
log.error("Error extracting user info from token", e);
}
} else {
log.warn("Access token validation failed");

// 토큰이 만료된 경우에도 정보 추출 시도 (선택적)
try {
Long userId = jwtUtil.getUserIdFromToken(accessToken);
String email = jwtUtil.getEmailFromToken(accessToken);
String nickname = jwtUtil.getNicknameFromToken(accessToken);

if (userId != null && email != null && nickname != null) {
user = User.builder()
.id(userId)
.email(email)
.nickname(nickname)
.role("USER")
.build();

// 새 토큰 발급 (쿠키 방식을 사용하는 경우만)
if (authHeader == null) {
String newAccessToken = jwtUtil.generateAccessToken(userId, email, nickname);
rq.setCrossDomainCookie("accessToken", newAccessToken, accessTokenExpiration);
log.info("New access token issued for user: {}", userId);
}
}
} catch (Exception e) {
log.error("Failed to extract user info from expired token", e);
}
log.warn("Access token validation failed - token is expired or invalid");
// 만료된 토큰은 인증 실패 처리 (user는 null로 유지)
}

// user가 null이면 인증 실패
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/com/back/global/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -31,15 +32,18 @@ public class SecurityConfig {
private final CustomOAuth2LoginSuccessHandler oauth2SuccessHandler;
private final CustomOAuth2LoginFailureHandler oauth2FailureHandler;
private final CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver;
private final CustomAuthenticationFilter customAuthenticationFilter;

public SecurityConfig(CustomOAuth2UserService customOAuth2UserService,
CustomOAuth2LoginSuccessHandler oauth2SuccessHandler,
CustomOAuth2LoginFailureHandler oauth2FailureHandler,
CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver) {
CustomOAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver,
CustomAuthenticationFilter customAuthenticationFilter) {
this.customOAuth2UserService = customOAuth2UserService;
this.oauth2SuccessHandler = oauth2SuccessHandler;
this.oauth2FailureHandler = oauth2FailureHandler;
this.customOAuth2AuthorizationRequestResolver = customOAuth2AuthorizationRequestResolver;
this.customAuthenticationFilter = customAuthenticationFilter;
}

@Bean
Expand All @@ -52,17 +56,19 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.maximumSessions(1)
) // OAuth 인증시 필요할때만 세션 사용

.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
// 개발 편의성을 위해 모든 요청 허용
.anyRequest().permitAll()


.requestMatchers("/user/auth/logout").authenticated()
/*
.requestMatchers("/").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/oauth2/**").permitAll()
.requestMatchers("/login/oauth2/**").permitAll()
.requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll()
.requestMatchers("/user/auth/refresh").permitAll()

// 권한 불필요 - 조회 API
.requestMatchers(GET, "/cocktails/**").permitAll()
Expand All @@ -80,6 +86,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 나머지 모든 API는 인증 필요
.anyRequest().authenticated()
*/
// 개발 편의성을 위해 모든 요청 허용
.anyRequest().permitAll()
)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ custom:
jwt:
secretKey: ${JWT_SECRET_KEY}
accessToken:
expirationSeconds: "#{60*15}"
expirationSeconds: "#{60}" # 15분 곱하기
refreshToken:
expirationSeconds: "#{60*60*24*30}"
idleTimeoutHours: "#{1}"
# "#{60*6*4}"


management:
Expand Down