diff --git a/backend/src/main/java/io/f1/backend/domain/user/app/CustomOAuthUserService.java b/backend/src/main/java/io/f1/backend/domain/user/app/CustomOAuthUserService.java index 61fd8bd6..2dfd3471 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/app/CustomOAuthUserService.java +++ b/backend/src/main/java/io/f1/backend/domain/user/app/CustomOAuthUserService.java @@ -1,8 +1,10 @@ package io.f1.backend.domain.user.app; +import static io.f1.backend.domain.user.constants.SessionKeys.*; + import io.f1.backend.domain.stat.entity.Stat; import io.f1.backend.domain.user.dao.UserRepository; -import io.f1.backend.domain.user.dto.SessionUser; +import io.f1.backend.domain.user.dto.AuthenticationUser; import io.f1.backend.domain.user.dto.UserPrincipal; import io.f1.backend.domain.user.entity.User; @@ -41,7 +43,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic .map(this::updateLastLogin) .orElseGet(() -> createNewUser(provider, providerId)); - httpSession.setAttribute("OAuthUser", new SessionUser(user)); + httpSession.setAttribute(OAUTH_USER, AuthenticationUser.from(user)); return new UserPrincipal(user, oAuth2User.getAttributes()); } diff --git a/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java b/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java index 57e27746..c7e48e56 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java +++ b/backend/src/main/java/io/f1/backend/domain/user/app/UserService.java @@ -1,7 +1,11 @@ package io.f1.backend.domain.user.app; +import static io.f1.backend.domain.user.constants.SessionKeys.OAUTH_USER; +import static io.f1.backend.domain.user.constants.SessionKeys.USER; +import static io.f1.backend.domain.user.mapper.UserMapper.toSignupResponse; + import io.f1.backend.domain.user.dao.UserRepository; -import io.f1.backend.domain.user.dto.SessionUser; +import io.f1.backend.domain.user.dto.AuthenticationUser; import io.f1.backend.domain.user.dto.SignupRequestDto; import io.f1.backend.domain.user.dto.SignupResponseDto; import io.f1.backend.domain.user.entity.User; @@ -22,28 +26,29 @@ public class UserService { @Transactional public SignupResponseDto signup(HttpSession session, SignupRequestDto signupRequest) { - SessionUser sessionUser = extractSessionUser(session); + AuthenticationUser authenticationUser = extractSessionUser(session); String nickname = signupRequest.nickname(); - validateNickname(nickname); - validateDuplicateNickname(nickname); + validateNicknameFormat(nickname); + validateNicknameDuplicate(nickname); - User user = updateUserNickname(sessionUser.getUserId(), nickname); + User user = updateUserNickname(authenticationUser.userId(), nickname); updateSessionAfterSignup(session, user); SecurityUtils.setAuthentication(user); - return SignupResponseDto.toDto(user); + return toSignupResponse(user); } - private SessionUser extractSessionUser(HttpSession session) { - SessionUser sessionUser = (SessionUser) session.getAttribute("OAuthUser"); - if (sessionUser == null) { - throw new RuntimeException("세션에 OAuth 정보 없음"); + private AuthenticationUser extractSessionUser(HttpSession session) { + AuthenticationUser authenticationUser = + (AuthenticationUser) session.getAttribute(OAUTH_USER); + if (authenticationUser == null) { + throw new RuntimeException("E401001: 로그인이 필요합니다."); } - return sessionUser; + return authenticationUser; } - private void validateNickname(String nickname) { + private void validateNicknameFormat(String nickname) { if (nickname == null || nickname.trim().isEmpty()) { throw new RuntimeException("E400002: 닉네임은 필수 입력입니다."); } @@ -55,22 +60,26 @@ private void validateNickname(String nickname) { } } - private void validateDuplicateNickname(String nickname) { + @Transactional(readOnly = true) + public void validateNicknameDuplicate(String nickname) { if (userRepository.existsUserByNickname(nickname)) { - throw new RuntimeException("닉네임 중복"); + throw new RuntimeException("E409001: 중복된 닉네임입니다."); } } - private User updateUserNickname(Long userId, String nickname) { + @Transactional + public User updateUserNickname(Long userId, String nickname) { User user = - userRepository.findById(userId).orElseThrow(() -> new RuntimeException("사용자 없음")); + userRepository + .findById(userId) + .orElseThrow(() -> new RuntimeException("E404001: 존재하지 않는 회원입니다.")); user.updateNickname(nickname); return userRepository.save(user); } private void updateSessionAfterSignup(HttpSession session, User user) { - session.removeAttribute("OAuthUser"); - session.setAttribute("user", new SessionUser(user)); + session.removeAttribute(OAUTH_USER); + session.setAttribute(USER, AuthenticationUser.from(user)); } } diff --git a/backend/src/main/java/io/f1/backend/domain/user/constants/SessionKeys.java b/backend/src/main/java/io/f1/backend/domain/user/constants/SessionKeys.java new file mode 100644 index 00000000..dc549453 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/user/constants/SessionKeys.java @@ -0,0 +1,9 @@ +package io.f1.backend.domain.user.constants; + +public final class SessionKeys { + + private SessionKeys() {} + + public static final String OAUTH_USER = "OAuthUser"; + public static final String USER = "user"; +} diff --git a/backend/src/main/java/io/f1/backend/domain/user/dto/AuthenticationUser.java b/backend/src/main/java/io/f1/backend/domain/user/dto/AuthenticationUser.java new file mode 100644 index 00000000..0a71a914 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/user/dto/AuthenticationUser.java @@ -0,0 +1,13 @@ +package io.f1.backend.domain.user.dto; + +import io.f1.backend.domain.user.entity.User; + +import java.io.Serializable; + +public record AuthenticationUser(Long userId, String nickname, String providerId) + implements Serializable { + + public static AuthenticationUser from(User user) { + return new AuthenticationUser(user.getId(), user.getNickname(), user.getProviderId()); + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/user/dto/SessionUser.java b/backend/src/main/java/io/f1/backend/domain/user/dto/SessionUser.java deleted file mode 100644 index 80aeb4ed..00000000 --- a/backend/src/main/java/io/f1/backend/domain/user/dto/SessionUser.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.f1.backend.domain.user.dto; - -import io.f1.backend.domain.user.entity.User; - -import lombok.Getter; - -import java.io.Serializable; -import java.time.LocalDateTime; - -@Getter -public class SessionUser implements Serializable { - - private final Long userId; - private final String nickname; - private final String providerId; - private final LocalDateTime lastLogin; - - public SessionUser(User user) { - this.userId = user.getId(); - this.nickname = user.getNickname(); - this.providerId = user.getProviderId(); - this.lastLogin = user.getLastLogin(); - } -} diff --git a/backend/src/main/java/io/f1/backend/domain/user/dto/SignupResponseDto.java b/backend/src/main/java/io/f1/backend/domain/user/dto/SignupResponseDto.java index f0593a3a..96ebbec2 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/dto/SignupResponseDto.java +++ b/backend/src/main/java/io/f1/backend/domain/user/dto/SignupResponseDto.java @@ -1,22 +1,3 @@ package io.f1.backend.domain.user.dto; -import io.f1.backend.domain.user.entity.User; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SignupResponseDto { - - private Long id; - private String nickname; - - public static SignupResponseDto toDto(User user) { - return SignupResponseDto.builder().id(user.getId()).nickname(user.getNickname()).build(); - } -} +public record SignupResponseDto(Long id, String nickname) {} diff --git a/backend/src/main/java/io/f1/backend/domain/user/dto/UserPrincipal.java b/backend/src/main/java/io/f1/backend/domain/user/dto/UserPrincipal.java index ef82fcf8..21efe07b 100644 --- a/backend/src/main/java/io/f1/backend/domain/user/dto/UserPrincipal.java +++ b/backend/src/main/java/io/f1/backend/domain/user/dto/UserPrincipal.java @@ -16,11 +16,11 @@ public class UserPrincipal implements UserDetails, OAuth2User { public static final String ROLE_USER = "ROLE_USER"; - private final User user; + private final AuthenticationUser authenticationUser; private final Map attributes; public UserPrincipal(User user, Map attributes) { - this.user = user; + this.authenticationUser = AuthenticationUser.from(user); this.attributes = attributes; } @@ -30,16 +30,16 @@ public Map getAttributes() { } public Long getUserId() { - return user.getId(); + return authenticationUser.userId(); } public String getUserNickname() { - return user.getNickname(); + return authenticationUser.nickname(); } @Override public String getName() { - return user.getProviderId(); + return authenticationUser.providerId(); } @Override @@ -54,7 +54,7 @@ public String getPassword() { @Override public String getUsername() { - return user.getProviderId(); + return authenticationUser.providerId(); } @Override diff --git a/backend/src/main/java/io/f1/backend/domain/user/mapper/UserMapper.java b/backend/src/main/java/io/f1/backend/domain/user/mapper/UserMapper.java new file mode 100644 index 00000000..1fd129a8 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/user/mapper/UserMapper.java @@ -0,0 +1,13 @@ +package io.f1.backend.domain.user.mapper; + +import io.f1.backend.domain.user.dto.SignupResponseDto; +import io.f1.backend.domain.user.entity.User; + +public class UserMapper { + + private UserMapper() {} + + public static SignupResponseDto toSignupResponse(User user) { + return new SignupResponseDto(user.getId(), user.getNickname()); + } +} diff --git a/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java b/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java index 57c98ac4..ebcfc42e 100644 --- a/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java +++ b/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java @@ -58,7 +58,9 @@ public SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception { .logoutSuccessHandler(oAuthLogoutSuccessHandler) .clearAuthentication(true) .invalidateHttpSession(true) - .permitAll()); + .permitAll()) + .sessionManagement( + session -> session.sessionFixation().migrateSession().maximumSessions(1)); return http.build(); } } diff --git a/backend/src/main/java/io/f1/backend/global/util/SecurityUtils.java b/backend/src/main/java/io/f1/backend/global/util/SecurityUtils.java index cd00c053..5915e5d2 100644 --- a/backend/src/main/java/io/f1/backend/global/util/SecurityUtils.java +++ b/backend/src/main/java/io/f1/backend/global/util/SecurityUtils.java @@ -4,6 +4,7 @@ import io.f1.backend.domain.user.entity.User; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import java.util.Collections; @@ -19,4 +20,21 @@ public static void setAuthentication(User user) { userPrincipal, null, userPrincipal.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } + + public static UserPrincipal getCurrentUserPrincipal() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null + && authentication.getPrincipal() instanceof UserPrincipal userPrincipal) { + return userPrincipal; + } + throw new RuntimeException("E401001: 로그인이 필요합니다."); + } + + public static Long getCurrentUserId() { + return getCurrentUserPrincipal().getUserId(); + } + + public static String getCurrentUserNickname() { + return getCurrentUserPrincipal().getUserNickname(); + } }