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
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,7 +18,7 @@
@Tag(name = "UserAuth", description = "사용자 인증 API")
@Slf4j
@RestController
@RequestMapping("/api/user/auth")
@RequestMapping("/user/auth")
@RequiredArgsConstructor
public class UserAuthController {

Expand All @@ -32,11 +33,11 @@ public class UserAuthController {
@ApiResponse(responseCode = "401", description = "토큰이 유효하지 않거나 만료됨")
})
@PostMapping("/refresh")
public RsData<Void> refreshToken(HttpServletRequest request, HttpServletResponse response) {
boolean success = userAuthService.refreshTokens(request, response);
public RsData<RefreshTokenResDto> 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, "토큰 갱신에 실패했습니다. 다시 로그인해주세요.");
}
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/back/domain/user/dto/RefreshTokenResDto.java
Original file line number Diff line number Diff line change
@@ -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;

}
}
24 changes: 7 additions & 17 deletions src/main/java/com/back/domain/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down
75 changes: 43 additions & 32 deletions src/main/java/com/back/domain/user/service/UserAuthService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,25 +25,25 @@
@RequiredArgsConstructor
public class UserAuthService {

static Set<String> param1 = Set.of("두둑한", "날씬한", "만취한", "알딸딸한", "얼큰한", "시트러스한", "도수높은", "톡쏘는", "거품가득한", "하이볼한",
"앙증맞은", "쓸쓸한", "거만한", "산만한", "귀찮은", "삐딱한", "맛이간", "저세상급", "시궁창스러운", "기묘한",
"졸린", "센치한", "철학적인", "무중력의", "뽀송한", "전투적인", "배부른", "대충한", "쩌는", "철지난",
"절규하는", "맞춤형", "다급한", "찌뿌둥한", "구수한", "문어발적인", "자포자기한", "터무니없는", "귀여운척하는",
"심드렁한", "무심한", "번쩍이는", "붉그레한", "밤새는", "좌절한", "의기양양한", "비굴한", "터프한", "흘러내린",
"공허한", "허무한", "헛기침하는", "뿜어대는", "질척한", "기어다니는", "헤매는", "삐죽한", "악에받친", "격렬한",
"삐까번쩍한", "오지랖넓은", "쪼르르거리는", "꿀꺽거리는", "머쓱한", "휘청대는", "추접스러운", "천방지축인", "어리둥절한", "질주하는",
"겸연쩍은", "뿌연", "썩은", "짠내나는", "철썩같은", "흥건한", "안간힘쓰는", "뜨끈한", "꾸덕한", "동공지진난",
"덕지덕지한", "비밀스러운", "개운한", "심란한", "음울한", "터질듯한", "달달한", "사악한", "기괴한", "용맹한",
"껄끄러운", "헐떡이는", "허둥대는", "분란스러운", "애매한", "찐득한", "허기진", "쩔어버린", "몽롱한", "허세떠는",
"황당한", "거대하고작은", "대차게구린", "어이없는", "두통약", "지갑", "이쑤시개", "돌침대", "고무장갑", "손수건",
"바람개비", "지하철표", "송진가루", "철가방", "머리끈", "양말한짝", "라이터", "숟가락", "스티커", "드럼통",
"열쇠꾸러미", "벼락", "대걸레", "파리채", "앙금빵", "선풍기날개", "스티로폼", "건전지", "껌종이", "소화전",
"비닐우산", "고드름", "전등갓", "양초", "지우개가루", "국자", "밥솥", "연필심", "비둘기깃털", "찜질팩",
"청테이프", "김밥말이", "곰팡이", "청소기", "밤송이", "옥수수수염", "철창살", "휴지심", "선반", "곽티슈",
"스프링노트", "고향집된장", "머드팩", "장독대", "뒤꿈치각질", "어묵꼬치", "환풍기", "군고구마", "카세트테이프",
"빨래건조대", "박카스병", "우체통", "주차권", "털실뭉치", "지하수", "깃털베개", "추리닝", "이불각", "육포",
"빨대", "지렁이분양소", "김칫국", "오징어채", "전기장판", "꽃병", "도시락통", "구급상자", "양배추잎", "고무줄",
"망치", "유통기한", "알람시계", "방범창", "신발깔창");
static Set<String> param1 = Set.of("두둑한", "날씬한", "만취한", "알딸딸", "얼큰한", "시트러스", "도수높은", "톡쏘는", "거품가득", "하이볼한",
"앙증맞은", "쓸쓸한", "거만한", "산만한", "귀찮은", "삐딱한", "맛이간", "저세상급", "시궁창", "기묘한",
"졸린", "센치한", "철학적인", "무중력", "뽀송한", "전투적인", "배부른", "대충한", "쩌는", "철지난",
"절규하는", "맞춤형", "다급한", "찌뿌둥한", "구수한", "문어발", "자포자기", "터무니", "귀척", "심드렁한",
"무심한", "번쩍이는", "붉그레한", "밤새는", "좌절한", "의기양양", "비굴한", "터프한", "흘러내린", "공허한",
"허무한", "헛기침", "뿜어대는", "질척한", "기어다님", "헤매는", "삐죽한", "악에받친", "격렬한", "삐까번쩍",
"오지랖", "쪼르르", "꿀꺽", "머쓱한", "휘청대는", "추접", "천방지축", "어리둥절", "질주하는", "겸연쩍은",
"뿌연", "썩은", "짠내나는", "철썩", "흥건한", "안간힘", "뜨끈한", "꾸덕한", "동공지진", "덕지덕지",
"비밀", "개운한", "심란한", "음울한", "터질듯한", "달달한", "사악한", "기괴한", "용맹한", "껄끄러운",
"헐떡이는", "허둥대는", "분란", "애매한", "찐득한", "허기진", "쩔어버린", "몽롱한", "허세", "황당한",
"거대작음", "대차게구림", "어이없음", "두통약", "지갑", "이쑤시개", "돌침대", "고무장갑", "손수건", "바람개비",
"지하철표", "송진가루", "철가방", "머리끈", "양말한짝", "라이터", "숟가락", "스티커", "드럼통", "열쇠",
"벼락", "대걸레", "파리채", "앙금빵", "날개", "스티로폼", "건전지", "껌종이", "소화전", "비닐우산",
"고드름", "전등갓", "양초", "지우개", "국자", "밥솥", "연필심", "깃털", "찜질팩", "청테이프",
"김밥말이", "곰팡이", "청소기", "밤송이", "옥수수", "철창살", "휴지심", "선반", "곽티슈", "스프링",
"고향된장", "머드팩", "장독대", "각질", "어묵꼬치", "환풍기", "군고구마", "카세트", "건조대", "박카스병",
"우체통", "주차권", "털실뭉치", "지하수", "추리닝", "이불각", "육포", "빨대", "지렁이", "김칫국",
"오징어채", "전기장판", "꽃병", "도시락통", "구급상자", "양배추잎", "고무줄", "망치", "유통기한", "알람시계",
"방범창", "깔창", "만취육포", "날씬국자", "터프각질", "음울밥솥", "사악김치", "허세숟갈", "삐딱곰팡");

static Set<String> param2 = Set.of("도토리딱개구리", "아프리카들개", "강남성인군자", "술고래", "알코올러버", "겨자잎", "청개구리", "산수유",
"맥주문어", "칵테일앵무새", "보드카수달", "진토닉거북이", "테킬라코요테", "럼펭귄", "사케고양이", "막걸리두꺼비",
Expand Down Expand Up @@ -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<RefreshToken> 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> user = userRepository.findById(userId);
if (user.isEmpty()) {
return false;
// DB에서 사용자 정보 조회
Optional<User> 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;
}
}

Expand Down
12 changes: 11 additions & 1 deletion src/main/java/com/back/global/controller/HomeController.java
Original file line number Diff line number Diff line change
@@ -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";
}
}
1 change: 1 addition & 0 deletions src/main/java/com/back/global/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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());
}

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