diff --git a/Dockerfile b/Dockerfile index eea0c19f..30d4b0ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,8 @@ COPY .env . COPY src src # 애플리케이션 빌드 -RUN gradle build --no-daemon +#RUN gradle build --no-daemon +RUN gradle build -x test --no-daemon # 두 번째 스테이지: 실행 스테이지 FROM container-registry.oracle.com/graalvm/jdk:21 diff --git a/src/main/java/com/back/domain/user/controller/UserAuthController.java b/src/main/java/com/back/domain/user/controller/UserAuthController.java index 6b587e7f..cf3e00cd 100644 --- a/src/main/java/com/back/domain/user/controller/UserAuthController.java +++ b/src/main/java/com/back/domain/user/controller/UserAuthController.java @@ -1,5 +1,6 @@ package com.back.domain.user.controller; +import com.back.domain.user.dto.RefreshTokenResDto; import com.back.domain.user.service.UserAuthService; import com.back.global.rsData.RsData; import io.swagger.v3.oas.annotations.Operation; @@ -17,7 +18,7 @@ @Tag(name = "UserAuth", description = "사용자 인증 API") @Slf4j @RestController -@RequestMapping("/api/user/auth") +@RequestMapping("/user/auth") @RequiredArgsConstructor public class UserAuthController { @@ -32,11 +33,11 @@ public class UserAuthController { @ApiResponse(responseCode = "401", description = "토큰이 유효하지 않거나 만료됨") }) @PostMapping("/refresh") - public RsData refreshToken(HttpServletRequest request, HttpServletResponse response) { - boolean success = userAuthService.refreshTokens(request, response); + public RsData refreshToken(HttpServletRequest request, HttpServletResponse response) { + RefreshTokenResDto refreshToken = userAuthService.refreshTokens(request, response); - if (success) { - return RsData.of(200, "토큰이 성공적으로 갱신되었습니다."); + if (refreshToken != null) { + return RsData.of(200, "토큰이 갱신 성공.", refreshToken); } else { return RsData.of(401, "토큰 갱신에 실패했습니다. 다시 로그인해주세요."); } diff --git a/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java b/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java new file mode 100644 index 00000000..3e705940 --- /dev/null +++ b/src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java @@ -0,0 +1,21 @@ +package com.back.domain.user.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RefreshTokenResDto { + private final String accessToken; + private final UserInfoDto user; + + @Getter + @Builder + public static class UserInfoDto { + private final String id; + private final String nickname; + private final Boolean isFirstLogin; + private final Double abvDegree; + + } +} diff --git a/src/main/java/com/back/domain/user/entity/User.java b/src/main/java/com/back/domain/user/entity/User.java index bb79c2b4..7c52b90c 100644 --- a/src/main/java/com/back/domain/user/entity/User.java +++ b/src/main/java/com/back/domain/user/entity/User.java @@ -1,28 +1,18 @@ package com.back.domain.user.entity; import com.back.domain.post.post.entity.PostLike; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import jakarta.persistence.*; +import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + @Entity @Table(name = "users") // 예약어 충돌 방지를 위해 "users" 권장 @Getter diff --git a/src/main/java/com/back/domain/user/service/UserAuthService.java b/src/main/java/com/back/domain/user/service/UserAuthService.java index eeb28888..72b5bc72 100644 --- a/src/main/java/com/back/domain/user/service/UserAuthService.java +++ b/src/main/java/com/back/domain/user/service/UserAuthService.java @@ -1,5 +1,6 @@ package com.back.domain.user.service; +import com.back.domain.user.dto.RefreshTokenResDto; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; import com.back.global.exception.ServiceException; @@ -24,25 +25,25 @@ @RequiredArgsConstructor public class UserAuthService { - static Set param1 = Set.of("두둑한", "날씬한", "만취한", "알딸딸한", "얼큰한", "시트러스한", "도수높은", "톡쏘는", "거품가득한", "하이볼한", - "앙증맞은", "쓸쓸한", "거만한", "산만한", "귀찮은", "삐딱한", "맛이간", "저세상급", "시궁창스러운", "기묘한", - "졸린", "센치한", "철학적인", "무중력의", "뽀송한", "전투적인", "배부른", "대충한", "쩌는", "철지난", - "절규하는", "맞춤형", "다급한", "찌뿌둥한", "구수한", "문어발적인", "자포자기한", "터무니없는", "귀여운척하는", - "심드렁한", "무심한", "번쩍이는", "붉그레한", "밤새는", "좌절한", "의기양양한", "비굴한", "터프한", "흘러내린", - "공허한", "허무한", "헛기침하는", "뿜어대는", "질척한", "기어다니는", "헤매는", "삐죽한", "악에받친", "격렬한", - "삐까번쩍한", "오지랖넓은", "쪼르르거리는", "꿀꺽거리는", "머쓱한", "휘청대는", "추접스러운", "천방지축인", "어리둥절한", "질주하는", - "겸연쩍은", "뿌연", "썩은", "짠내나는", "철썩같은", "흥건한", "안간힘쓰는", "뜨끈한", "꾸덕한", "동공지진난", - "덕지덕지한", "비밀스러운", "개운한", "심란한", "음울한", "터질듯한", "달달한", "사악한", "기괴한", "용맹한", - "껄끄러운", "헐떡이는", "허둥대는", "분란스러운", "애매한", "찐득한", "허기진", "쩔어버린", "몽롱한", "허세떠는", - "황당한", "거대하고작은", "대차게구린", "어이없는", "두통약", "지갑", "이쑤시개", "돌침대", "고무장갑", "손수건", - "바람개비", "지하철표", "송진가루", "철가방", "머리끈", "양말한짝", "라이터", "숟가락", "스티커", "드럼통", - "열쇠꾸러미", "벼락", "대걸레", "파리채", "앙금빵", "선풍기날개", "스티로폼", "건전지", "껌종이", "소화전", - "비닐우산", "고드름", "전등갓", "양초", "지우개가루", "국자", "밥솥", "연필심", "비둘기깃털", "찜질팩", - "청테이프", "김밥말이", "곰팡이", "청소기", "밤송이", "옥수수수염", "철창살", "휴지심", "선반", "곽티슈", - "스프링노트", "고향집된장", "머드팩", "장독대", "뒤꿈치각질", "어묵꼬치", "환풍기", "군고구마", "카세트테이프", - "빨래건조대", "박카스병", "우체통", "주차권", "털실뭉치", "지하수", "깃털베개", "추리닝", "이불각", "육포", - "빨대", "지렁이분양소", "김칫국", "오징어채", "전기장판", "꽃병", "도시락통", "구급상자", "양배추잎", "고무줄", - "망치", "유통기한", "알람시계", "방범창", "신발깔창"); + static Set param1 = Set.of("두둑한", "날씬한", "만취한", "알딸딸", "얼큰한", "시트러스", "도수높은", "톡쏘는", "거품가득", "하이볼한", + "앙증맞은", "쓸쓸한", "거만한", "산만한", "귀찮은", "삐딱한", "맛이간", "저세상급", "시궁창", "기묘한", + "졸린", "센치한", "철학적인", "무중력", "뽀송한", "전투적인", "배부른", "대충한", "쩌는", "철지난", + "절규하는", "맞춤형", "다급한", "찌뿌둥한", "구수한", "문어발", "자포자기", "터무니", "귀척", "심드렁한", + "무심한", "번쩍이는", "붉그레한", "밤새는", "좌절한", "의기양양", "비굴한", "터프한", "흘러내린", "공허한", + "허무한", "헛기침", "뿜어대는", "질척한", "기어다님", "헤매는", "삐죽한", "악에받친", "격렬한", "삐까번쩍", + "오지랖", "쪼르르", "꿀꺽", "머쓱한", "휘청대는", "추접", "천방지축", "어리둥절", "질주하는", "겸연쩍은", + "뿌연", "썩은", "짠내나는", "철썩", "흥건한", "안간힘", "뜨끈한", "꾸덕한", "동공지진", "덕지덕지", + "비밀", "개운한", "심란한", "음울한", "터질듯한", "달달한", "사악한", "기괴한", "용맹한", "껄끄러운", + "헐떡이는", "허둥대는", "분란", "애매한", "찐득한", "허기진", "쩔어버린", "몽롱한", "허세", "황당한", + "거대작음", "대차게구림", "어이없음", "두통약", "지갑", "이쑤시개", "돌침대", "고무장갑", "손수건", "바람개비", + "지하철표", "송진가루", "철가방", "머리끈", "양말한짝", "라이터", "숟가락", "스티커", "드럼통", "열쇠", + "벼락", "대걸레", "파리채", "앙금빵", "날개", "스티로폼", "건전지", "껌종이", "소화전", "비닐우산", + "고드름", "전등갓", "양초", "지우개", "국자", "밥솥", "연필심", "깃털", "찜질팩", "청테이프", + "김밥말이", "곰팡이", "청소기", "밤송이", "옥수수", "철창살", "휴지심", "선반", "곽티슈", "스프링", + "고향된장", "머드팩", "장독대", "각질", "어묵꼬치", "환풍기", "군고구마", "카세트", "건조대", "박카스병", + "우체통", "주차권", "털실뭉치", "지하수", "추리닝", "이불각", "육포", "빨대", "지렁이", "김칫국", + "오징어채", "전기장판", "꽃병", "도시락통", "구급상자", "양배추잎", "고무줄", "망치", "유통기한", "알람시계", + "방범창", "깔창", "만취육포", "날씬국자", "터프각질", "음울밥솥", "사악김치", "허세숟갈", "삐딱곰팡"); static Set param2 = Set.of("도토리딱개구리", "아프리카들개", "강남성인군자", "술고래", "알코올러버", "겨자잎", "청개구리", "산수유", "맥주문어", "칵테일앵무새", "보드카수달", "진토닉거북이", "테킬라코요테", "럼펭귄", "사케고양이", "막걸리두꺼비", @@ -134,46 +135,56 @@ public String generateNickname(String baseNickname) { public void issueTokens(HttpServletResponse response, Long userId, String email, String nickname) { String accessToken = jwtUtil.generateAccessToken(userId, email, nickname); - String refreshToken = refreshTokenService.generateRefreshToken(userId, email); + String refreshToken = refreshTokenService.generateRefreshToken(userId); jwtUtil.addAccessTokenToCookie(response, accessToken); jwtUtil.addRefreshTokenToCookie(response, refreshToken); } - public boolean refreshTokens(HttpServletRequest request, HttpServletResponse response) { + public RefreshTokenResDto refreshTokens(HttpServletRequest request, HttpServletResponse response) { try { String oldRefreshToken = jwtUtil.getRefreshTokenFromCookie(request); if (oldRefreshToken == null || !refreshTokenService.validateToken(oldRefreshToken)) { - return false; + return null; } Optional tokenData = refreshTokenRepository.findByToken(oldRefreshToken); if (tokenData.isEmpty()) { - return false; + return null; } RefreshToken refreshTokenEntity = tokenData.get(); Long userId = refreshTokenEntity.getUserId(); - String email = refreshTokenEntity.getEmail(); - // DB에서 현재 nickname 조회 - Optional user = userRepository.findById(userId); - if (user.isEmpty()) { - return false; + // DB에서 사용자 정보 조회 + Optional userOpt = userRepository.findById(userId); + if (userOpt.isEmpty()) { + return null; } - String nickname = user.get().getNickname(); + + User user = userOpt.get(); String newRefreshToken = refreshTokenService.rotateToken(oldRefreshToken); - String newAccessToken = jwtUtil.generateAccessToken(userId, email, nickname); + String newAccessToken = jwtUtil.generateAccessToken(userId, user.getEmail(), user.getNickname()); jwtUtil.addAccessTokenToCookie(response, newAccessToken); jwtUtil.addRefreshTokenToCookie(response, newRefreshToken); - return true; + return RefreshTokenResDto.builder() + .accessToken(newAccessToken) + .user( + RefreshTokenResDto.UserInfoDto.builder() + .id(user.getId().toString()) + .nickname(user.getNickname()) + .isFirstLogin(user.isFirstLogin()) + .abvDegree(user.getAbvDegree()) + .build() + ) + .build(); } catch (Exception e) { log.error("토큰 갱신 중 오류 발생: {}", e.getMessage()); - return false; + return null; } } diff --git a/src/main/java/com/back/global/controller/HomeController.java b/src/main/java/com/back/global/controller/HomeController.java index 14eeb2f8..e7a2f936 100644 --- a/src/main/java/com/back/global/controller/HomeController.java +++ b/src/main/java/com/back/global/controller/HomeController.java @@ -1,13 +1,23 @@ package com.back.global.controller; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HomeController { + @Value("${custom.site.frontUrl}") + private String frontUrl; + + @Value("${spring.profiles.active}") + private String activeProfile; + @GetMapping("/") - public String redirectToSwagger() { + public String redirect() { + if("prod".equals(activeProfile)){ + return "redirect:" + frontUrl; + } return "redirect:/swagger-ui/index.html"; } } \ No newline at end of file diff --git a/src/main/java/com/back/global/jwt/JwtUtil.java b/src/main/java/com/back/global/jwt/JwtUtil.java index e49108fb..36c19201 100644 --- a/src/main/java/com/back/global/jwt/JwtUtil.java +++ b/src/main/java/com/back/global/jwt/JwtUtil.java @@ -42,6 +42,7 @@ public String generateAccessToken(Long userId, String email, String nickname) { .compact(); } + public void addAccessTokenToCookie(HttpServletResponse response, String accessToken) { Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken); cookie.setHttpOnly(true); diff --git a/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java index a1f450c1..b139a66e 100644 --- a/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java +++ b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java @@ -20,21 +20,18 @@ public class RefreshToken { @Column(nullable = false) private Long userId; - @Column(nullable = false) - private String email; - @Column(nullable = false) private LocalDateTime createdAt; @Column(nullable = false) private LocalDateTime expiresAt; - public static RefreshToken create(String token, Long userId, String email, long ttlSeconds) { + + public static RefreshToken create(String token, Long userId, long ttlSeconds) { LocalDateTime now = LocalDateTime.now(); return RefreshToken.builder() .token(token) .userId(userId) - .email(email) .createdAt(now) .expiresAt(now.plusSeconds(ttlSeconds)) .build(); diff --git a/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java index fa6db9cb..c7972977 100644 --- a/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java +++ b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java @@ -5,14 +5,14 @@ import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.Base64; import java.util.Optional; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -26,12 +26,12 @@ public class RefreshTokenService { // 기존 리프레시 토큰 삭제하고 생성 @Transactional - public String generateRefreshToken(Long userId, String email) { + public String generateRefreshToken(Long userId) { // 기존 토큰 삭제 refreshTokenRepository.deleteByUserId(userId); String token = generateSecureToken(); - RefreshToken refreshToken = RefreshToken.create(token, userId, email, refreshTokenExpiration); + RefreshToken refreshToken = RefreshToken.create(token, userId, refreshTokenExpiration); refreshTokenRepository.save(refreshToken); return token; @@ -65,7 +65,7 @@ public String rotateToken(String oldToken) { RefreshToken tokenData = oldRefreshToken.get(); revokeToken(oldToken); - return generateRefreshToken(tokenData.getUserId(), tokenData.getEmail()); + return generateRefreshToken(tokenData.getUserId()); } //삭제 diff --git a/src/main/java/com/back/global/security/CustomOAuth2LoginSuccessHandler.java b/src/main/java/com/back/global/security/CustomOAuth2LoginSuccessHandler.java index e5f6a433..f7ea019b 100644 --- a/src/main/java/com/back/global/security/CustomOAuth2LoginSuccessHandler.java +++ b/src/main/java/com/back/global/security/CustomOAuth2LoginSuccessHandler.java @@ -24,19 +24,15 @@ public class CustomOAuth2LoginSuccessHandler implements AuthenticationSuccessHan @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); + // Access Token과 Refresh Token 발급 userAuthService.issueTokens(response, securityUser.getId(), securityUser.getEmail(), securityUser.getNickname()); - // 첫 로그인 여부에 따라 리다이렉트 분기 - String redirectUrl; - if (securityUser.isFirstLogin()) { - redirectUrl = frontendUrl + "/oauth/success/welcome"; userAuthService.setFirstLoginFalse(securityUser.getId()); + response.sendRedirect(frontendUrl + "/login/first-user"); } else { - redirectUrl = frontendUrl + "/oauth/success"; + response.sendRedirect(frontendUrl + "/login/success"); } - - response.sendRedirect(redirectUrl); } } \ No newline at end of file