diff --git a/src/main/java/corecord/dev/common/exception/GeneralExceptionAdvice.java b/src/main/java/corecord/dev/common/exception/GeneralExceptionAdvice.java index 917d45f..47579de 100644 --- a/src/main/java/corecord/dev/common/exception/GeneralExceptionAdvice.java +++ b/src/main/java/corecord/dev/common/exception/GeneralExceptionAdvice.java @@ -3,6 +3,7 @@ import corecord.dev.common.response.ApiResponse; import corecord.dev.common.status.ErrorStatus; import corecord.dev.domain.token.exception.model.TokenException; +import corecord.dev.domain.user.exception.model.UserException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -14,6 +15,13 @@ @RestControllerAdvice(annotations = {RestController.class}) public class GeneralExceptionAdvice extends ResponseEntityExceptionHandler { + // UserException 처리 + @ExceptionHandler(UserException.class) + public ResponseEntity> handleUserException(UserException e) { + log.warn(">>>>>>>>UserException: {}", e.getUserErrorStatus().getMessage()); + return ApiResponse.error(e.getUserErrorStatus()); + } + // TokenException 처리 @ExceptionHandler(TokenException.class) public ResponseEntity> handleTokenException(TokenException e) { diff --git a/src/main/java/corecord/dev/common/status/ErrorStatus.java b/src/main/java/corecord/dev/common/status/ErrorStatus.java index f9425dd..0965c6b 100644 --- a/src/main/java/corecord/dev/common/status/ErrorStatus.java +++ b/src/main/java/corecord/dev/common/status/ErrorStatus.java @@ -17,16 +17,16 @@ public enum ErrorStatus implements BaseErrorStatus { * 404 : 존재하지 않는 정보에 대한 요청. */ - BAD_REQUEST(HttpStatus.BAD_REQUEST, "E400", "잘못된 요청입니다."), - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E401", "인증이 필요합니다."), - FORBIDDEN(HttpStatus.FORBIDDEN, "E403", "접근 권한이 없습니다."), - NOT_FOUND(HttpStatus.NOT_FOUND, "E404", "요청한 자원을 찾을 수 없습니다."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, "E0400", "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E0401", "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "E0403", "접근 권한이 없습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "E0404", "요청한 자원을 찾을 수 없습니다."), /** * Error Code * 500 : 서버 내부 오류 */ - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500", "서버 내부 오류입니다."); + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E9999", "서버 내부 오류입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/corecord/dev/common/util/CookieUtil.java b/src/main/java/corecord/dev/common/util/CookieUtil.java index c21cc6b..d05275b 100644 --- a/src/main/java/corecord/dev/common/util/CookieUtil.java +++ b/src/main/java/corecord/dev/common/util/CookieUtil.java @@ -7,36 +7,51 @@ import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component @RequiredArgsConstructor public class CookieUtil { @Value("${jwt.refresh-token.expiration-time}") private long refreshTokenExpirationTime; - public ResponseCookie createRefreshTokenCookie(String refreshToken) { - return ResponseCookie.from("refresh_token", refreshToken) + @Value("${jwt.register-token.expiration-time}") + private long registerTokenExpirationTime; + + @Value("${jwt.access-token.expiration-time}") + private long accessTokenExpirationTime; + + public ResponseCookie createTokenCookie(String tokenName, String token) { + long expirationTime = switch (tokenName) { + case "refreshToken" -> refreshTokenExpirationTime; + case "registerToken" -> registerTokenExpirationTime; + case "accessToken" -> accessTokenExpirationTime; + default -> throw new IllegalArgumentException("Unexpected value: " + tokenName); + }; + + return ResponseCookie.from(tokenName, token) .httpOnly(true) - .sameSite("None") // 배포 시 수정 - .secure(false) // 배포 시 수정 + .secure(false) // 배포 시 true로 설정 + .sameSite("None") .path("/") - .maxAge(refreshTokenExpirationTime) + .maxAge(expirationTime / 1000) // maxAge는 초 단위 .build(); } - public Cookie getCookie(HttpServletRequest request) { + public String getCookieValue(HttpServletRequest request, String cookieName) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { - if (cookie.getName().equals("refresh_token")) { - return cookie; + if (cookie.getName().equals(cookieName)) { + return cookie.getValue(); } } } return null; } - public Cookie deleteRefreshTokenCookie() { - Cookie cookie = new Cookie("refresh_token", ""); + public Cookie deleteCookie(String cookieName) { + Cookie cookie = new Cookie(cookieName, ""); cookie.setMaxAge(0); cookie.setPath("/"); return cookie; diff --git a/src/main/java/corecord/dev/common/util/JwtFilter.java b/src/main/java/corecord/dev/common/util/JwtFilter.java index 5edab66..0767c27 100644 --- a/src/main/java/corecord/dev/common/util/JwtFilter.java +++ b/src/main/java/corecord/dev/common/util/JwtFilter.java @@ -22,7 +22,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String token = resolveToken(request); if (token != null && jwtUtil.isAccessTokenValid(token)) { - String userId = jwtUtil.getUserIdFromAcccessToken(token).toString(); + String userId = jwtUtil.getUserIdFromAccessToken(token).toString(); Authentication authToken = new UsernamePasswordAuthenticationToken( userId, // principal로 userId 사용 null, // credentials는 필요 없으므로 null diff --git a/src/main/java/corecord/dev/common/util/JwtUtil.java b/src/main/java/corecord/dev/common/util/JwtUtil.java index b3673fd..f5039f5 100644 --- a/src/main/java/corecord/dev/common/util/JwtUtil.java +++ b/src/main/java/corecord/dev/common/util/JwtUtil.java @@ -2,6 +2,7 @@ import corecord.dev.domain.token.exception.enums.TokenErrorStatus; import corecord.dev.domain.token.exception.model.TokenException; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @@ -61,50 +62,48 @@ public String generateRegisterToken(String providerId) { .compact(); } + public boolean isRegisterTokenValid(String token) { + return isTokenValid(token, "providerId", TokenErrorStatus.INVALID_REGISTER_TOKEN); + } + public boolean isAccessTokenValid(String token) { - return isTokenValid(token, "userId"); + return isTokenValid(token, "userId", TokenErrorStatus.INVALID_ACCESS_TOKEN); } - public boolean isRegisterTokenValid(String token) { - return isTokenValid(token, "providerId"); + public boolean isRefreshTokenValid(String token) { + return isTokenValid(token, "userId", TokenErrorStatus.INVALID_REFRESH_TOKEN); } - private boolean isTokenValid(String token, String claimKey) { + private boolean isTokenValid(String token, String claimKey, TokenErrorStatus invalidTokenErrorStatus) { try { var claims = Jwts.parser() .verifyWith(this.getSigningKey()) .build() .parseSignedClaims(token); - // 토큰 만료 여부 확인 Date expirationDate = claims.getPayload().getExpiration(); if (expirationDate.before(new Date())) { - log.warn("토큰이 만료되었습니다."); - return false; // 만료된 토큰 + log.warn("{}이 만료되었습니다.", invalidTokenErrorStatus.getMessage()); + throw new TokenException(invalidTokenErrorStatus); } - // 필수 클레임이 있는지 확인 String claimValue = claims.getPayload().get(claimKey, String.class); if (claimValue == null || claimValue.isEmpty()) { log.warn("토큰에 {} 클레임이 없습니다.", claimKey); - return false; // 필수 클레임이 없는 경우 + throw new TokenException(invalidTokenErrorStatus); } - return true; // 유효한 토큰 + return true; + + } catch (ExpiredJwtException e) { + log.warn("토큰이 만료되었습니다: {}", e.getMessage()); + throw new TokenException(invalidTokenErrorStatus); } catch (JwtException e) { - log.warn("유효하지 않은 토큰입니다. JWT 예외: {}", e.getMessage()); - return false; - } catch (IllegalArgumentException e) { - log.warn("잘못된 토큰 형식입니다. 예외: {}", e.getMessage()); - return false; + log.warn("유효하지 않은 토큰입니다: {}", e.getMessage()); + throw new TokenException(invalidTokenErrorStatus); } } - - public String getTokenFromHeader(String authorizationHeader) { - return authorizationHeader.substring(7); - } - public String getProviderIdFromToken(String token) { try { log.info("토큰 파싱"); @@ -121,7 +120,7 @@ public String getProviderIdFromToken(String token) { } } - public String getUserIdFromAcccessToken(String token) { + public String getUserIdFromAccessToken(String token) { try { return Jwts.parser() .verifyWith(this.getSigningKey()) diff --git a/src/main/java/corecord/dev/domain/auth/application/OAuthLoginSuccessHandler.java b/src/main/java/corecord/dev/domain/auth/application/OAuthLoginSuccessHandler.java index 36ac954..8bfa41f 100644 --- a/src/main/java/corecord/dev/domain/auth/application/OAuthLoginSuccessHandler.java +++ b/src/main/java/corecord/dev/domain/auth/application/OAuthLoginSuccessHandler.java @@ -20,7 +20,6 @@ import org.springframework.stereotype.Component; import java.io.IOException; -import java.net.URLEncoder; import java.util.Optional; @Slf4j @@ -28,10 +27,10 @@ @RequiredArgsConstructor public class OAuthLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Value("${jwt.redirect.access}") - private String ACCESS_TOKEN_REDIRECT_URI; // 기존 유저 로그인 시 리다이렉트 URI + private String ACCESS_TOKEN_REDIRECT_URI; @Value("${jwt.redirect.register}") - private String REGISTER_TOKEN_REDIRECT_URI; // 신규 유저 로그인 시 리다이렉트 URI + private String REGISTER_TOKEN_REDIRECT_URI; private final JwtUtil jwtUtil; private final CookieUtil cookieUtil; @@ -49,29 +48,75 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo log.info("name: {}", name); Optional optionalUser = userRepository.findByProviderId(providerId); - User user; if (optionalUser.isPresent()) { - log.info("기존 유저입니다. 액세스 토큰과 리프레쉬 토큰을 발급합니다."); - user = optionalUser.get(); - refreshTokenRepository.deleteByUserId(user.getUserId()); - String refreshToken = jwtUtil.generateRefreshToken(user.getUserId()); - RefreshToken newRefreshToken = RefreshToken.builder().userId(user.getUserId()).refreshToken(refreshToken).build(); - refreshTokenRepository.save(newRefreshToken); - - ResponseCookie cookie = cookieUtil.createRefreshTokenCookie(refreshToken); - response.addHeader("Set-Cookie", cookie.toString()); - - String accessToken = URLEncoder.encode(jwtUtil.generateAccessToken(user.getUserId())); - String redirectURI = String.format(ACCESS_TOKEN_REDIRECT_URI, accessToken); - getRedirectStrategy().sendRedirect(request, response, redirectURI); + handleExistingUser(request, response, optionalUser.get()); } else { - log.info("신규 유저입니다. 레지스터 토큰을 발급합니다."); - String registerToken = URLEncoder.encode(jwtUtil.generateRegisterToken(providerId)); - String redirectURI = String.format(REGISTER_TOKEN_REDIRECT_URI, registerToken); - getRedirectStrategy().sendRedirect(request, response, redirectURI); + handleNewUser(request, response, providerId); } } + // 기존 유저 처리 + private void handleExistingUser(HttpServletRequest request, HttpServletResponse response, User user) throws IOException { + log.info("기존 유저입니다. 액세스 토큰과 리프레쉬 토큰을 발급합니다."); + // 기존 리프레쉬 토큰 삭제 + refreshTokenRepository.deleteByUserId(user.getUserId()); + + // 새로운 리프레쉬 토큰 생성 및 저장 + String refreshToken = jwtUtil.generateRefreshToken(user.getUserId()); + saveRefreshToken(user, refreshToken); + + // 기존 쿠키 삭제 + deleteExistingTokens(response); + + // RefreshToken 쿠키 추가 + ResponseCookie refreshTokenCookie = cookieUtil.createTokenCookie("refreshToken", refreshToken); + response.addHeader("Set-Cookie", refreshTokenCookie.toString()); + + // AccessToken 쿠키 추가 + String accessToken = jwtUtil.generateAccessToken(user.getUserId()); + ResponseCookie accessTokenCookie = cookieUtil.createTokenCookie("accessToken", accessToken); + response.addHeader("Set-Cookie", accessTokenCookie.toString()); + + String redirectURI = String.format(ACCESS_TOKEN_REDIRECT_URI, accessToken, refreshToken); + + // 액세스 토큰 리다이렉트 + getRedirectStrategy().sendRedirect(request, response, redirectURI); + } + + // 신규 유저 처리 + private void handleNewUser(HttpServletRequest request, HttpServletResponse response, String providerId) throws IOException { + log.info("신규 유저입니다. 레지스터 토큰을 발급합니다."); + + // 이미 레지스터 토큰이 있다면 삭제 + response.addCookie(cookieUtil.deleteCookie("registerToken")); + + // 레지스터 토큰 발급 + String registerToken = jwtUtil.generateRegisterToken(providerId); + + // 레지스터 토큰 쿠키 설정 + ResponseCookie registerTokenCookie = cookieUtil.createTokenCookie("registerToken", registerToken); + response.addHeader("Set-Cookie", registerTokenCookie.toString()); + + String redirectURI = String.format(REGISTER_TOKEN_REDIRECT_URI, registerToken); + + // 레지스터 토큰 리다이렉트 + getRedirectStrategy().sendRedirect(request, response, redirectURI); + } + + // 리프레쉬 토큰 저장 + private void saveRefreshToken(User user, String refreshToken) { + RefreshToken newRefreshToken = RefreshToken.builder() + .userId(user.getUserId()) + .refreshToken(refreshToken) + .build(); + refreshTokenRepository.save(newRefreshToken); + } + + // 기존 토큰 삭제 + private void deleteExistingTokens(HttpServletResponse response) { + response.addCookie(cookieUtil.deleteCookie("accessToken")); + response.addCookie(cookieUtil.deleteCookie("refreshToken")); + } } diff --git a/src/main/java/corecord/dev/domain/token/converter/TokenConverter.java b/src/main/java/corecord/dev/domain/token/converter/TokenConverter.java new file mode 100644 index 0000000..27bd9ff --- /dev/null +++ b/src/main/java/corecord/dev/domain/token/converter/TokenConverter.java @@ -0,0 +1,9 @@ +package corecord.dev.domain.token.converter; + +import corecord.dev.domain.token.dto.response.TokenResponse; + +public class TokenConverter { + public static TokenResponse.AccessTokenResponse toAccessTokenResponse(String newAccessToken) { + return TokenResponse.AccessTokenResponse.builder().accessToken(newAccessToken).build(); + } +} diff --git a/src/main/java/corecord/dev/domain/token/exception/enums/TokenErrorStatus.java b/src/main/java/corecord/dev/domain/token/exception/enums/TokenErrorStatus.java index 13e7c08..d40e442 100644 --- a/src/main/java/corecord/dev/domain/token/exception/enums/TokenErrorStatus.java +++ b/src/main/java/corecord/dev/domain/token/exception/enums/TokenErrorStatus.java @@ -8,10 +8,11 @@ @Getter @AllArgsConstructor public enum TokenErrorStatus implements BaseErrorStatus { - INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "E001", "유효하지 않은 액세스 토큰입니다."), - INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "E002", "유효하지 않은 리프레쉬 토큰입니다."), - INVALID_REGISTER_TOKEN(HttpStatus.UNAUTHORIZED, "E003", "유효하지 않은 회원가입 토큰입니다."), - REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E004", "해당 유저 ID의 리프레쉬 토큰이 없습니다."); + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_ACCESS", "유효하지 않은 액세스 토큰입니다."), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_REFRESH", "유효하지 않은 리프레쉬 토큰입니다."), + INVALID_REGISTER_TOKEN(HttpStatus.UNAUTHORIZED, "E0400_REGISTER", "유효하지 않은 회원가입 토큰입니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_REFRESH", "해당 유저 ID의 리프레쉬 토큰이 없습니다."), + REGISTER_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_REGISTER", "회원가입 토큰이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/corecord/dev/domain/token/service/TokenService.java b/src/main/java/corecord/dev/domain/token/service/TokenService.java index 43de21a..83dfbb3 100644 --- a/src/main/java/corecord/dev/domain/token/service/TokenService.java +++ b/src/main/java/corecord/dev/domain/token/service/TokenService.java @@ -2,12 +2,12 @@ import corecord.dev.common.util.CookieUtil; import corecord.dev.common.util.JwtUtil; +import corecord.dev.domain.token.converter.TokenConverter; import corecord.dev.domain.token.dto.response.TokenResponse; import corecord.dev.domain.token.entity.RefreshToken; import corecord.dev.domain.token.exception.enums.TokenErrorStatus; import corecord.dev.domain.token.exception.model.TokenException; import corecord.dev.domain.token.repository.RefreshTokenRepository; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; @@ -24,21 +24,48 @@ public class TokenService { @Transactional public TokenResponse.AccessTokenResponse reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { - Cookie cookie = cookieUtil.getCookie(request); - String refreshToken = cookie.getValue(); + // RefreshToken 추출 및 유효성 검증 + String refreshToken = getRefreshTokenFromCookie(request); + + // RefreshToken이 유효한지 확인 Long userId = Long.parseLong(jwtUtil.getUserIdFromRefreshToken(refreshToken)); - RefreshToken existRefreshToken = getExistRefreshToken(refreshToken); - if (!existRefreshToken.getRefreshToken().equals(refreshToken) || !(jwtUtil.isAccessTokenValid(refreshToken))) { + validateRefreshToken(refreshToken); + + // 새 AccessToken 발급 + String newAccessToken = jwtUtil.generateAccessToken(userId); + + // AccessToken 쿠키 생성 + setAccessTokenCookie(response, newAccessToken); + + return TokenConverter.toAccessTokenResponse(newAccessToken); + } + + // 쿠키에서 RefreshToken 가져오기 + private String getRefreshTokenFromCookie(HttpServletRequest request) { + String refreshToken = cookieUtil.getCookieValue(request, "refreshToken"); + if (refreshToken == null) { + throw new TokenException(TokenErrorStatus.REFRESH_TOKEN_NOT_FOUND); + } + return refreshToken; + } + + // RefreshToken 검증 + private void validateRefreshToken(String refreshToken) { + RefreshToken existingRefreshToken = getExistingRefreshToken(refreshToken); + if (!existingRefreshToken.getRefreshToken().equals(refreshToken) || !jwtUtil.isRefreshTokenValid(refreshToken)) { throw new TokenException(TokenErrorStatus.INVALID_REFRESH_TOKEN); } - String newAccessToken = jwtUtil.generateAccessToken(userId); - ResponseCookie newCookie = cookieUtil.createRefreshTokenCookie(refreshToken); - response.addHeader("Set-Cookie", newCookie.toString()); - return TokenResponse.AccessTokenResponse.builder().accessToken(newAccessToken).build(); } - private RefreshToken getExistRefreshToken(String refreshToken) { + // DB에서 RefreshToken 확인 + private RefreshToken getExistingRefreshToken(String refreshToken) { return refreshTokenRepository.findByRefreshToken(refreshToken) .orElseThrow(() -> new TokenException(TokenErrorStatus.REFRESH_TOKEN_NOT_FOUND)); } + + // AccessToken 쿠키 생성 및 응답 헤더에 추가 + private void setAccessTokenCookie(HttpServletResponse response, String accessToken) { + ResponseCookie accessTokenCookie = cookieUtil.createTokenCookie("accessToken", accessToken); + response.addHeader("Set-Cookie", accessTokenCookie.toString()); + } } diff --git a/src/main/java/corecord/dev/domain/user/controller/UserController.java b/src/main/java/corecord/dev/domain/user/controller/UserController.java index a6cc9e7..d8df0fe 100644 --- a/src/main/java/corecord/dev/domain/user/controller/UserController.java +++ b/src/main/java/corecord/dev/domain/user/controller/UserController.java @@ -7,6 +7,7 @@ import corecord.dev.domain.user.dto.request.UserRequest; import corecord.dev.domain.user.dto.response.UserResponse; import corecord.dev.domain.user.service.UserService; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -28,10 +29,10 @@ public ResponseEntity> getSuccess( @PostMapping("/register") public ResponseEntity> registerUser( HttpServletResponse response, - @RequestHeader("Authorization") String authorizationHeader, - @RequestBody UserRequest.UserRegisterDto request + HttpServletRequest request, + @RequestBody UserRequest.UserRegisterDto userRegisterDto ) { - UserResponse.UserRegisterDto registerResponse = userService.registerUser(response, authorizationHeader, request); + UserResponse.UserRegisterDto registerResponse = userService.registerUser(response, request, userRegisterDto); return ApiResponse.success(UserSuccessStatus.USER_REGISTER_SUCCESS, registerResponse); } } diff --git a/src/main/java/corecord/dev/domain/user/converter/UserConverter.java b/src/main/java/corecord/dev/domain/user/converter/UserConverter.java index ccf2215..ab006e5 100644 --- a/src/main/java/corecord/dev/domain/user/converter/UserConverter.java +++ b/src/main/java/corecord/dev/domain/user/converter/UserConverter.java @@ -2,6 +2,7 @@ import corecord.dev.domain.user.dto.request.UserRequest; import corecord.dev.domain.user.dto.response.UserResponse; +import corecord.dev.domain.user.entity.Status; import corecord.dev.domain.user.entity.User; public class UserConverter { @@ -10,7 +11,7 @@ public static User toUserEntity(UserRequest.UserRegisterDto request, String prov return User.builder() .providerId(providerId) .nickName(request.getNickName()) - .status(request.getStatus()) + .status(Status.getStatus(request.getStatus())) .build(); } @@ -18,7 +19,7 @@ public static UserResponse.UserRegisterDto toUserRegisterDto(User user, String a return UserResponse.UserRegisterDto.builder() .userId(user.getUserId()) .nickname(user.getNickName()) - .status(user.getStatus()) + .status(user.getStatus().getValue()) .accessToken(accessToken) .build(); } diff --git a/src/main/java/corecord/dev/domain/user/entity/Status.java b/src/main/java/corecord/dev/domain/user/entity/Status.java new file mode 100644 index 0000000..f2381dc --- /dev/null +++ b/src/main/java/corecord/dev/domain/user/entity/Status.java @@ -0,0 +1,30 @@ +package corecord.dev.domain.user.entity; + +import corecord.dev.domain.user.exception.enums.UserErrorStatus; +import corecord.dev.domain.user.exception.model.UserException; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum Status { + UNIVERSITY_STUDENT("대학생"), + GRADUATE_STUDENT("대학원생"), + JOB_SEEKER("취업 준비생"), + INTERN("인턴"), + EMPLOYED("재직 중"), + OTHER("기타"); + + private final String value; + + public String getValue() { + return value; + } + + public static Status getStatus(String value) { + for (Status status : values()) { + if (status.getValue().equals(value)) { + return status; + } + } + throw new UserException(UserErrorStatus.INVALID_USER_STATUS); + } +} diff --git a/src/main/java/corecord/dev/domain/user/entity/User.java b/src/main/java/corecord/dev/domain/user/entity/User.java index d58ebd0..a80d955 100644 --- a/src/main/java/corecord/dev/domain/user/entity/User.java +++ b/src/main/java/corecord/dev/domain/user/entity/User.java @@ -24,12 +24,14 @@ public class User extends BaseEntity { @Column(nullable = false) private String nickName; + @Enumerated(EnumType.STRING) @Column(nullable = false) - private String status; + private Status status; @Column private Integer tmpChat; @Column private Integer tmpMemo; + } diff --git a/src/main/java/corecord/dev/domain/user/exception/enums/UserErrorStatus.java b/src/main/java/corecord/dev/domain/user/exception/enums/UserErrorStatus.java new file mode 100644 index 0000000..835c94c --- /dev/null +++ b/src/main/java/corecord/dev/domain/user/exception/enums/UserErrorStatus.java @@ -0,0 +1,17 @@ +package corecord.dev.domain.user.exception.enums; + +import corecord.dev.common.base.BaseErrorStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum UserErrorStatus implements BaseErrorStatus { + INVALID_USER_NICKNAME(HttpStatus.BAD_REQUEST, "E101_NICKNAME", "유효하지 않은 닉네임입니다."), + INVALID_USER_STATUS(HttpStatus.BAD_REQUEST, "E101_STATUS", "신분상태의 입력이 잘못되었습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/corecord/dev/domain/user/exception/model/UserException.java b/src/main/java/corecord/dev/domain/user/exception/model/UserException.java new file mode 100644 index 0000000..60c8ddf --- /dev/null +++ b/src/main/java/corecord/dev/domain/user/exception/model/UserException.java @@ -0,0 +1,16 @@ +package corecord.dev.domain.user.exception.model; + +import corecord.dev.domain.user.exception.enums.UserErrorStatus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class UserException extends RuntimeException { + private final UserErrorStatus userErrorStatus; + + @Override + public String getMessage() { + return userErrorStatus.getMessage(); + } +} diff --git a/src/main/java/corecord/dev/domain/user/service/UserService.java b/src/main/java/corecord/dev/domain/user/service/UserService.java index 12fc348..d9a0a00 100644 --- a/src/main/java/corecord/dev/domain/user/service/UserService.java +++ b/src/main/java/corecord/dev/domain/user/service/UserService.java @@ -10,7 +10,10 @@ import corecord.dev.domain.user.dto.request.UserRequest; import corecord.dev.domain.user.dto.response.UserResponse; import corecord.dev.domain.user.entity.User; +import corecord.dev.domain.user.exception.enums.UserErrorStatus; +import corecord.dev.domain.user.exception.model.UserException; import corecord.dev.domain.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -18,39 +21,97 @@ import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; +import java.util.regex.Pattern; + @Slf4j @Service @RequiredArgsConstructor public class UserService { + private final JwtUtil jwtUtil; private final CookieUtil cookieUtil; private final UserRepository userRepository; private final RefreshTokenRepository refreshTokenRepository; @Transactional - public UserResponse.UserRegisterDto registerUser(HttpServletResponse response, String authorizationHeader, UserRequest.UserRegisterDto request) { - String registerToken = jwtUtil.getTokenFromHeader(authorizationHeader); + public UserResponse.UserRegisterDto registerUser(HttpServletResponse response, HttpServletRequest request, UserRequest.UserRegisterDto userRegisterDto) { + // 쿠키에서 registerToken 가져오기 + String registerToken = getRegisterTokenFromCookie(request); + + // registerToken 유효성 검증 validRegisterToken(registerToken); + // user 정보 유효성 검증 + validateUserRegisterInfo(userRegisterDto); + + // 새로운 유저 생성 String providerId = jwtUtil.getProviderIdFromToken(registerToken); - User newUser = UserConverter.toUserEntity(request, providerId); - User user = userRepository.save(newUser); + User newUser = UserConverter.toUserEntity(userRegisterDto, providerId); + User savedUser = userRepository.save(newUser); - String refreshToken = jwtUtil.generateRefreshToken(user.getUserId()); - RefreshToken newRefreshToken = RefreshToken.of(refreshToken, user.getUserId()); - refreshTokenRepository.save(newRefreshToken); + // RefreshToken 생성 및 저장 + String refreshToken = jwtUtil.generateRefreshToken(savedUser.getUserId()); + saveRefreshToken(refreshToken, savedUser); - ResponseCookie cookie = cookieUtil.createRefreshTokenCookie(refreshToken); - response.addHeader("Set-Cookie", cookie.toString()); + // registerToken 쿠키 삭제 + addDeleteCookieHeader(response, "registerToken"); - String accessToken = jwtUtil.generateAccessToken(user.getUserId()); - return UserConverter.toUserRegisterDto(user, accessToken); + // 새 RefreshToken 및 AccessToken 쿠키 설정 + setTokenCookies(response, refreshToken, savedUser); + + return UserConverter.toUserRegisterDto(savedUser, jwtUtil.generateAccessToken(savedUser.getUserId())); + } + + // 쿠키에서 registerToken 가져오기 + private String getRegisterTokenFromCookie(HttpServletRequest request) { + String registerToken = cookieUtil.getCookieValue(request, "registerToken"); + if (registerToken == null) { + throw new TokenException(TokenErrorStatus.REGISTER_TOKEN_NOT_FOUND); + } + return registerToken; } + // registerToken 유효성 검증 private void validRegisterToken(String registerToken) { - if(!jwtUtil.isRegisterTokenValid(registerToken)) { + if (!jwtUtil.isRegisterTokenValid(registerToken)) { throw new TokenException(TokenErrorStatus.INVALID_REGISTER_TOKEN); } } + // RefreshToken 저장 + private void saveRefreshToken(String refreshToken, User user) { + RefreshToken newRefreshToken = RefreshToken.of(refreshToken, user.getUserId()); + refreshTokenRepository.save(newRefreshToken); + } + + // 토큰 쿠키 설정 + private void setTokenCookies(HttpServletResponse response, String refreshToken, User user) { + // RefreshToken 쿠키 추가 + ResponseCookie refreshTokenCookie = cookieUtil.createTokenCookie("refreshToken", refreshToken); + response.addHeader("Set-Cookie", refreshTokenCookie.toString()); + + // AccessToken 쿠키 추가 + String accessToken = jwtUtil.generateAccessToken(user.getUserId()); + ResponseCookie accessTokenCookie = cookieUtil.createTokenCookie("accessToken", accessToken); + response.addHeader("Set-Cookie", accessTokenCookie.toString()); + } + + // 쿠키 삭제 헤더 추가 + private void addDeleteCookieHeader(HttpServletResponse response, String cookieName) { + response.addCookie(cookieUtil.deleteCookie(cookieName)); + } + + // user 정보 유효성 검증 + private void validateUserRegisterInfo(UserRequest.UserRegisterDto userRegisterDto) { + String nickName = userRegisterDto.getNickName(); + if (nickName == null || nickName.isEmpty() || nickName.length() > 10) { + throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); + } + + // 한글, 영어, 숫자, 공백만 허용 + String nicknamePattern = "^[a-zA-Z0-9가-힣\\s]*$"; + if (!Pattern.matches(nicknamePattern, nickName)) { + throw new UserException(UserErrorStatus.INVALID_USER_NICKNAME); + } + } }