From 175f2b6953faafec81965ff6199351eb25695688 Mon Sep 17 00:00:00 2001 From: CYY1007 Date: Sun, 6 Jul 2025 14:32:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle | 6 +- .../rootboxApp/RootboxAppApplication.java | 2 + .../api/user/business/UserService.java | 85 +++++++++++++++++++ .../implementation/UserCommandAdapter.java | 41 +++++++++ .../user/implementation/UserQueryAdapter.java | 15 ++++ .../persistence/RefreshTokenRepository.java | 11 +++ .../api/user/persistence/UserRepository.java | 4 + .../api/user/presentation/UserApi.java | 32 +++++-- .../user/presentation/dto/SocialLoginDto.java | 37 ++++++++ .../common/exception/ExceptionAdvice.java | 23 +++++ .../CustomFeignClientException.java | 10 +++ .../exception/base/GlobalErrorCode.java | 1 + .../global/entity/RefreshToken.java | 24 ++++++ .../rootboxApp/global/entity/User.java | 8 ++ .../global/entity/common/BaseEntity.java | 4 +- .../feign/client/KakaoInfoFeignClient.java | 18 ++++ .../feign/client/KakaotestFeignClient.java | 27 ++++++ .../feign/config/KakaoFeignConfiguration.java | 19 +++++ .../feign/dto/KakaoSocialKakaoAccountDto.java | 36 ++++++++ .../feign/dto/KakaoSocialPartnerDto.java | 10 +++ .../feign/dto/KakaoSocialProfileDto.java | 16 ++++ .../global/feign/dto/KakaoSocialUserDto.java | 20 +++++ .../global/feign/dto/KakaoTokenDto.java | 15 ++++ .../feign/dto/KakaoTokenRequestDto.java | 16 ++++ .../global/feign/dto/OAuthInfoDto.java | 14 +++ .../FeignClientExceptionErrorDecoder.java | 30 +++++++ .../global/feign/mapper/KakaoOAuthMapper.java | 19 +++++ .../feign/service/KakaoOauthService.java | 57 +++++++++++++ .../security/config/SecurityConfig.java | 3 +- src/main/resources/application.yml | 14 +++ 31 files changed, 608 insertions(+), 10 deletions(-) create mode 100644 src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java create mode 100644 src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java create mode 100644 src/main/java/rootbox/rootboxApp/api/user/presentation/dto/SocialLoginDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/ThrowClass/CustomFeignClientException.java create mode 100644 src/main/java/rootbox/rootboxApp/global/entity/RefreshToken.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/client/KakaoInfoFeignClient.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/client/KakaotestFeignClient.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/config/KakaoFeignConfiguration.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialKakaoAccountDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialPartnerDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialProfileDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialUserDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenRequestDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/dto/OAuthInfoDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/exception/FeignClientExceptionErrorDecoder.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/mapper/KakaoOAuthMapper.java create mode 100644 src/main/java/rootbox/rootboxApp/global/feign/service/KakaoOauthService.java diff --git a/.gitignore b/.gitignore index c2065bc..44a0429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.env build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ diff --git a/build.gradle b/build.gradle index 9e1cb48..39b2854 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.0' + id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.7' } @@ -49,6 +49,10 @@ dependencies { //validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // feign + + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3' } tasks.named('test') { diff --git a/src/main/java/rootbox/rootboxApp/RootboxAppApplication.java b/src/main/java/rootbox/rootboxApp/RootboxAppApplication.java index b4bfeba..f65dae5 100644 --- a/src/main/java/rootbox/rootboxApp/RootboxAppApplication.java +++ b/src/main/java/rootbox/rootboxApp/RootboxAppApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients public class RootboxAppApplication { public static void main(String[] args) { diff --git a/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java index 960b89f..c0f00d8 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java +++ b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java @@ -2,10 +2,23 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import rootbox.rootboxApp.api.user.implementation.UserCommandAdapter; import rootbox.rootboxApp.api.user.implementation.UserQueryAdapter; +import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto; +import rootbox.rootboxApp.global.entity.RefreshToken; import rootbox.rootboxApp.global.entity.User; +import rootbox.rootboxApp.global.entity.enums.user.SocialType; +import rootbox.rootboxApp.global.entity.enums.user.UserRole; +import rootbox.rootboxApp.global.feign.dto.OAuthInfoDto; +import rootbox.rootboxApp.global.feign.service.KakaoOauthService; +import rootbox.rootboxApp.global.security.provider.TokenProvider; +import java.util.List; import java.util.Optional; @Service @@ -15,8 +28,80 @@ public class UserService { private final UserQueryAdapter userQueryAdapter; + private final UserCommandAdapter userCommandAdapter; + + private final KakaoOauthService kakaoOauthService; + + private final TokenProvider tokenProvider; + Optional findById(String id) { return userQueryAdapter.findUserByIdSecurity(id); } + + @Transactional + public SocialLoginDto.KakaoSocialLoginResponseDto socialLogin(SocialLoginDto.KakaoSocialLoginRequestDto request) { + + String kakaoToken = request.getKakaoToken(); + String requestToken = "Bearer " + kakaoToken; + + OAuthInfoDto kakaoUserInfo = kakaoOauthService.getKakaoUserInfo(requestToken); + + Optional userBySocialId = userQueryAdapter.findUserBySocialId(kakaoUserInfo.getId()); + + // 로그인 처리 + if (userBySocialId.isPresent()) { + Optional refreshTokenByUserId = userQueryAdapter.findRefreshTokenByUserId(kakaoUserInfo.getId()); + + String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name()))); + + // 리프레시 토큰이 존재할 때 + if (refreshTokenByUserId.isPresent()) { + return SocialLoginDto.KakaoSocialLoginResponseDto.builder() + .accessToken(accessToken) + .isNew(false) + .loginType(SocialType.KAKAO.name()) + .refreshToken(refreshTokenByUserId.get().getRefreshToken()) + .build(); + }else{ + // 리프레시 토큰 없음 만약 만료된 리프레시 토큰이면 추후에 만료 로직 탈 것이라 존재 유무만 봄 + return SocialLoginDto.KakaoSocialLoginResponseDto.builder() + .accessToken(accessToken) + .isNew(false) + .loginType(SocialType.KAKAO.name()) + .refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(), + userBySocialId.get().getSocialLoginUid()).getRefreshToken()) + .build(); + } + }else { + // 신규 가입 + 로그인 + User user = userCommandAdapter.createUser(kakaoUserInfo.getId(), generateUniqueNickname()); + + String accessToken = tokenProvider.createAccessToken(user, List.of(new SimpleGrantedAuthority(UserRole.USER.name()))); + return SocialLoginDto.KakaoSocialLoginResponseDto + .builder() + .loginType(SocialType.KAKAO.name()) + .isNew(true) + .accessToken(accessToken) + .refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(), + user.getSocialLoginUid()).getRefreshToken()) + .build(); + } + } + + public String getKakaoCode(){ + return kakaoOauthService.getKakaoCodeUrl(); + } + + public String getKakaoToken(String code){ + return kakaoOauthService.getKakaoAccessToken(code); + } + + private String generateUniqueNickname() { + String name = ""; + do { + name = RandomStringUtils.random(8, true, true); + } while (userQueryAdapter.findUserByNickname(name).isPresent()); + return name; + } } diff --git a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java new file mode 100644 index 0000000..cbb172e --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserCommandAdapter.java @@ -0,0 +1,41 @@ +package rootbox.rootboxApp.api.user.implementation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import rootbox.rootboxApp.api.user.persistence.RefreshTokenRepository; +import rootbox.rootboxApp.api.user.persistence.UserRepository; +import rootbox.rootboxApp.global.annotations.Adapter; +import rootbox.rootboxApp.global.entity.RefreshToken; +import rootbox.rootboxApp.global.entity.User; +import rootbox.rootboxApp.global.entity.enums.user.UserRole; + +@Adapter +@Slf4j +@RequiredArgsConstructor +public class UserCommandAdapter { + + private final UserRepository userRepository; + + private final RefreshTokenRepository refreshTokenRepository; + + public User createUser(String username, String socialUid){ + + User newUser = User.builder() + .socialLoginUid(socialUid) + .nickname(username) + .userRole(UserRole.USER) + .build(); + + return userRepository.save(newUser); + } + + public RefreshToken saveRefreshToken(String refreshToken, String userSocialId){ + + return refreshTokenRepository.save( + RefreshToken.builder() + .userSocialId(userSocialId) + .refreshToken(refreshToken) + .build() + ); + } +} diff --git a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java index 1f87d7c..20ee823 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java +++ b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java @@ -1,8 +1,10 @@ package rootbox.rootboxApp.api.user.implementation; import lombok.RequiredArgsConstructor; +import rootbox.rootboxApp.api.user.persistence.RefreshTokenRepository; import rootbox.rootboxApp.api.user.persistence.UserRepository; import rootbox.rootboxApp.global.annotations.Adapter; +import rootbox.rootboxApp.global.entity.RefreshToken; import rootbox.rootboxApp.global.entity.User; import java.util.Optional; @@ -13,8 +15,21 @@ public class UserQueryAdapter { private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + public Optional findUserByIdSecurity(String userId){ return userRepository.findById(Long.valueOf(userId)); } + public Optional findUserByNickname(String nickname){ + return userRepository.findByNickname(nickname); + } + + public Optional findUserBySocialId(String socialId){ + return userRepository.findBySocialLoginUid(socialId); + } + + public Optional findRefreshTokenByUserId(String userId){ + return refreshTokenRepository.findByUserSocialId(userId); + } } diff --git a/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java b/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java new file mode 100644 index 0000000..517d3f3 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/persistence/RefreshTokenRepository.java @@ -0,0 +1,11 @@ +package rootbox.rootboxApp.api.user.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import rootbox.rootboxApp.global.entity.RefreshToken; + +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + + Optional findByUserSocialId(String userId); +} diff --git a/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java b/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java index c939bd6..7b12fa2 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java +++ b/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java @@ -8,4 +8,8 @@ public interface UserRepository extends JpaRepository { public Optional findById(Long id); + + public Optional findByNickname(String nickname); + + public Optional findBySocialLoginUid(String socialId); } diff --git a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java index 9e67d29..8ec23c5 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java +++ b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java @@ -1,22 +1,42 @@ package rootbox.rootboxApp.api.user.presentation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; -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; +import org.springframework.web.bind.annotation.*; +import rootbox.rootboxApp.api.user.business.UserService; +import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto; +import rootbox.rootboxApp.global.common.CommonResponse; + +import java.io.IOException; @RestController @RequiredArgsConstructor @Slf4j @Validated @Tag(name = "User Api", description = "rootbox 사용자 관련 Api입니다.") -@RequestMapping(value = "/api/v1/user") +@RequestMapping(value = "/api/v1/users") public class UserApi { - @GetMapping(value = "/health") + private final UserService userService; + + @GetMapping(value = "/auth/health") public String health() {return "I'm healthy!!!" ;} + + @PostMapping(value = "/auth/kakao") + public CommonResponse kakaoSocialLogin(@RequestBody @Valid SocialLoginDto.KakaoSocialLoginRequestDto requestDto) { + return CommonResponse.onSuccess(userService.socialLogin(requestDto)); + } + @GetMapping(value = "/auth/kakao/code") + public void kakaoSocailLoginTest(HttpServletResponse response) throws IOException { + response.sendRedirect(userService.getKakaoCode()); + } + + @GetMapping(value = "/auth/kakao/test") + public CommonResponse getKakaoToken(@RequestParam("code") String code){ + return CommonResponse.onSuccess(userService.getKakaoToken(code)); + } } diff --git a/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/SocialLoginDto.java b/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/SocialLoginDto.java new file mode 100644 index 0000000..6da4f91 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/presentation/dto/SocialLoginDto.java @@ -0,0 +1,37 @@ +package rootbox.rootboxApp.api.user.presentation.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.*; + + +public class SocialLoginDto { + + @Builder + @Getter + @Setter + public static class KakaoSocialLoginResponseDto { + + @NotNull + String loginType; + + @NotNull + Boolean isNew; + + @NotNull + String accessToken; + + @NotNull + String refreshToken; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class KakaoSocialLoginRequestDto { + + @NotNull + String kakaoToken; + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java b/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java index 6d935ca..7fa14dc 100644 --- a/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java @@ -7,7 +7,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.User; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -31,6 +33,27 @@ @RestControllerAdvice(annotations = {RestController.class}) public class ExceptionAdvice extends ResponseEntityExceptionHandler { + @Override + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + + CommonResponse body = CommonResponse.onFailure( + GlobalErrorCode.BAD_BODY.getCode(), + "요청 본문을 읽을 수 없습니다. BODY 자체를 읽을 수 없는 상태입니다.. (형식 오류)", + null + ); + + return super.handleExceptionInternal( + ex, + body, + headers, + HttpStatus.BAD_REQUEST, + request + ); + } + @org.springframework.web.bind.annotation.ExceptionHandler public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/ThrowClass/CustomFeignClientException.java b/src/main/java/rootbox/rootboxApp/global/common/exception/ThrowClass/CustomFeignClientException.java new file mode 100644 index 0000000..7a27168 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/ThrowClass/CustomFeignClientException.java @@ -0,0 +1,10 @@ +package rootbox.rootboxApp.global.common.exception.ThrowClass; + +import rootbox.rootboxApp.global.common.exception.base.BaseErrorCode; +import rootbox.rootboxApp.global.common.exception.base.GeneralException; + +public class CustomFeignClientException extends GeneralException { + public CustomFeignClientException(BaseErrorCode errorCode){ + super(errorCode); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java index 25d78ca..fb9fa09 100644 --- a/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java @@ -12,6 +12,7 @@ public enum GlobalErrorCode implements BaseErrorCode{ + BAD_BODY(HttpStatus.BAD_REQUEST, "GLOBAL400", "요청 BODY 본문을 읽을 수 없습니다"), // AUTH + 401 Unauthorized - 권한 없음 TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_1", "인증 토큰이 만료 되었습니다. 토큰을 재발급 해주세요"), INVALID_TOKEN(UNAUTHORIZED, "AUTH401_2", "인증 토큰이 유효하지 않습니다."), diff --git a/src/main/java/rootbox/rootboxApp/global/entity/RefreshToken.java b/src/main/java/rootbox/rootboxApp/global/entity/RefreshToken.java new file mode 100644 index 0000000..03f8d09 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/entity/RefreshToken.java @@ -0,0 +1,24 @@ +package rootbox.rootboxApp.global.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "refresh_token") +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String refreshToken; + + private String userSocialId; +} diff --git a/src/main/java/rootbox/rootboxApp/global/entity/User.java b/src/main/java/rootbox/rootboxApp/global/entity/User.java index 06a6af1..56585f7 100644 --- a/src/main/java/rootbox/rootboxApp/global/entity/User.java +++ b/src/main/java/rootbox/rootboxApp/global/entity/User.java @@ -5,6 +5,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; import rootbox.rootboxApp.global.entity.common.BaseEntity; import rootbox.rootboxApp.global.entity.enums.user.SocialType; import rootbox.rootboxApp.global.entity.enums.user.UserRole; @@ -20,6 +22,8 @@ @Builder @AllArgsConstructor @NoArgsConstructor +@DynamicInsert +@DynamicUpdate @Table(name = "user") public class User extends BaseEntity { @@ -52,6 +56,10 @@ public class User extends BaseEntity { private Integer travelPhotoWn = 0; + private Integer getAlarmYn = 0; + + private Integer locationServiceYn = 0; + @OneToMany(mappedBy = "user") private List routeList = new ArrayList<>(); diff --git a/src/main/java/rootbox/rootboxApp/global/entity/common/BaseEntity.java b/src/main/java/rootbox/rootboxApp/global/entity/common/BaseEntity.java index 135dff0..d90d13c 100644 --- a/src/main/java/rootbox/rootboxApp/global/entity/common/BaseEntity.java +++ b/src/main/java/rootbox/rootboxApp/global/entity/common/BaseEntity.java @@ -17,10 +17,10 @@ public abstract class BaseEntity { @Column(name = "deleteYn") private Integer deleteYn = 0; - @Column(nullable = false, updatable = false, name = "reg_date") + @Column(nullable = false, updatable = false, name = "reg_date", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") @CreatedDate private LocalDateTime regDate; - @Column(nullable = false,name = "mod_date") + @Column(nullable = false, name = "mod_date", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") private LocalDateTime modDate; } diff --git a/src/main/java/rootbox/rootboxApp/global/feign/client/KakaoInfoFeignClient.java b/src/main/java/rootbox/rootboxApp/global/feign/client/KakaoInfoFeignClient.java new file mode 100644 index 0000000..a839c52 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/client/KakaoInfoFeignClient.java @@ -0,0 +1,18 @@ +package rootbox.rootboxApp.global.feign.client; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import rootbox.rootboxApp.global.feign.config.KakaoFeignConfiguration; +import rootbox.rootboxApp.global.feign.dto.KakaoSocialUserDto; + +@FeignClient(name = "KakaoInfoFeignClient", url = "${oauth.kakao.baseUrl}", configuration = KakaoFeignConfiguration.class) +@Component +public interface KakaoInfoFeignClient { + + @GetMapping("/v2/user/me") + KakaoSocialUserDto getInfo(@RequestHeader(name = "Authorization") String Authorization); + + +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/client/KakaotestFeignClient.java b/src/main/java/rootbox/rootboxApp/global/feign/client/KakaotestFeignClient.java new file mode 100644 index 0000000..3d3c6c5 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/client/KakaotestFeignClient.java @@ -0,0 +1,27 @@ +package rootbox.rootboxApp.global.feign.client; + +import feign.Headers; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import rootbox.rootboxApp.global.feign.config.KakaoFeignConfiguration; +import rootbox.rootboxApp.global.feign.dto.KakaoTokenDto; +import rootbox.rootboxApp.global.feign.dto.KakaoTokenRequestDto; + +import java.util.Map; + +@FeignClient(name = "KakaoTestFeignClient", url = "https://kauth.kakao.com", configuration = KakaoFeignConfiguration.class) +@Component +public interface KakaotestFeignClient { + + @GetMapping("/oauth/authorize") + public void getCode (@RequestParam("response_type") String type, @RequestParam("client_id") String client_id, @RequestParam("redirect_uri") String redirect_uri); + + @PostMapping(value = "/oauth/token" ,consumes = "application/x-www-form-urlencoded;charset=utf-8") + @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8") + public KakaoTokenDto getToken( + @RequestParam Map params); +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/config/KakaoFeignConfiguration.java b/src/main/java/rootbox/rootboxApp/global/feign/config/KakaoFeignConfiguration.java new file mode 100644 index 0000000..c55291f --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/config/KakaoFeignConfiguration.java @@ -0,0 +1,19 @@ +package rootbox.rootboxApp.global.feign.config; + +import feign.Logger; +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import rootbox.rootboxApp.global.feign.exception.FeignClientExceptionErrorDecoder; + +public class KakaoFeignConfiguration { + + @Bean + public ErrorDecoder errorDecoder() { + return new FeignClientExceptionErrorDecoder(); + } + + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialKakaoAccountDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialKakaoAccountDto.java new file mode 100644 index 0000000..80b8e3e --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialKakaoAccountDto.java @@ -0,0 +1,36 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +public class KakaoSocialKakaoAccountDto { + + private Boolean profile_needs_agreement; + private Boolean profile_nickname_needs_agreement; + private Boolean profile_image_needs_agreement; + private KakaoSocialProfileDto profile; + private Boolean name_needs_agreement; + private String name; + private Boolean email_needs_agreement; + private Boolean is_email_valid; + private Boolean is_email_verified; + private String email; + private Boolean age_range_needs_agreement; + private String age_range; + private Boolean birthyear_needs_agreement; + private String birthyear; + private Boolean birthday_needs_agreement; + private String birthday; + private String birthday_type; + private Boolean gender_needs_agreement; + private String gender; + private Boolean phone_number_needs_agreement; + private String phone_number; + private Boolean ci_needs_agreement; + private String ci; + private LocalDateTime ci_authenticated_at; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialPartnerDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialPartnerDto.java new file mode 100644 index 0000000..fb9ab24 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialPartnerDto.java @@ -0,0 +1,10 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +public class KakaoSocialPartnerDto { + private String uuid; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialProfileDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialProfileDto.java new file mode 100644 index 0000000..eb783c9 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialProfileDto.java @@ -0,0 +1,16 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class KakaoSocialProfileDto { + + private String nickname; + private String thumbnail_image_url; + private String profile_image_url; + private Boolean is_default_image; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialUserDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialUserDto.java new file mode 100644 index 0000000..8bb60a2 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoSocialUserDto.java @@ -0,0 +1,20 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +public class KakaoSocialUserDto { + + private Long id; + private Boolean has_signed_up; + private LocalDateTime connected_at; + private LocalDateTime synched_at; + private Object properties; + private KakaoSocialKakaoAccountDto kakao_account; + private KakaoSocialPartnerDto for_partner; + +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenDto.java new file mode 100644 index 0000000..d5e987b --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenDto.java @@ -0,0 +1,15 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class KakaoTokenDto { + + private String token_type; + private String access_token; + private String id_token; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenRequestDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenRequestDto.java new file mode 100644 index 0000000..0367f42 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/KakaoTokenRequestDto.java @@ -0,0 +1,16 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class KakaoTokenRequestDto { + + private String grant_type; + private String client_id; + private String redirect_uri; + private String code; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/dto/OAuthInfoDto.java b/src/main/java/rootbox/rootboxApp/global/feign/dto/OAuthInfoDto.java new file mode 100644 index 0000000..0b22970 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/dto/OAuthInfoDto.java @@ -0,0 +1,14 @@ +package rootbox.rootboxApp.global.feign.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OAuthInfoDto { + + private String email; + private String id; +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/exception/FeignClientExceptionErrorDecoder.java b/src/main/java/rootbox/rootboxApp/global/feign/exception/FeignClientExceptionErrorDecoder.java new file mode 100644 index 0000000..90f8e96 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/exception/FeignClientExceptionErrorDecoder.java @@ -0,0 +1,30 @@ +package rootbox.rootboxApp.global.feign.exception; + +import feign.Response; +import feign.codec.ErrorDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rootbox.rootboxApp.global.common.exception.ThrowClass.CustomFeignClientException; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; + +public class FeignClientExceptionErrorDecoder implements ErrorDecoder { + + Logger logger = LoggerFactory.getLogger(FeignClientExceptionErrorDecoder.class); + + @Override + public Exception decode(String methodKey, Response response) { + + String requestUrl = "Unknown URL"; + if (response.request() != null) { + requestUrl = response.request().url(); + } + + if (response.status() >= 400 && response.status() <= 499) { + logger.error("{}번 에러 발생 at {} : 에러 사유 : {}, 에러 사유가 null인 경우를 대비 : {}, 요청 REQUEST 정보는 : {}", response.status(),requestUrl, response.reason(), response, response.request()); + return new CustomFeignClientException(GlobalErrorCode.FEIGN_CLIENT_ERROR_400); + } else { + logger.error("500번대 에러 발생 at : {} 에러 사유 : {} 요청 REQUEST 정보는 : {}",requestUrl, response.reason(), response.request()); + return new CustomFeignClientException(GlobalErrorCode.FEIGN_CLIENT_ERROR_500); + } + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/mapper/KakaoOAuthMapper.java b/src/main/java/rootbox/rootboxApp/global/feign/mapper/KakaoOAuthMapper.java new file mode 100644 index 0000000..f3697c2 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/mapper/KakaoOAuthMapper.java @@ -0,0 +1,19 @@ +package rootbox.rootboxApp.global.feign.mapper; + +import rootbox.rootboxApp.global.feign.dto.KakaoSocialUserDto; +import rootbox.rootboxApp.global.feign.dto.KakaoTokenDto; +import rootbox.rootboxApp.global.feign.dto.OAuthInfoDto; + +public class KakaoOAuthMapper { + + public static OAuthInfoDto toOAuthInfoDto(KakaoSocialUserDto kakaoSocialUserDto){ + return OAuthInfoDto.builder() + .email(kakaoSocialUserDto.getKakao_account().getEmail()) + .id(String.valueOf(kakaoSocialUserDto.getId())) + .build(); + } + + public static String toKakaoToken(KakaoTokenDto requestDto){ + return requestDto.getAccess_token(); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/feign/service/KakaoOauthService.java b/src/main/java/rootbox/rootboxApp/global/feign/service/KakaoOauthService.java new file mode 100644 index 0000000..0610990 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/feign/service/KakaoOauthService.java @@ -0,0 +1,57 @@ +package rootbox.rootboxApp.global.feign.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import rootbox.rootboxApp.global.feign.client.KakaoInfoFeignClient; +import rootbox.rootboxApp.global.feign.client.KakaotestFeignClient; +import rootbox.rootboxApp.global.feign.dto.KakaoSocialUserDto; +import rootbox.rootboxApp.global.feign.dto.KakaoTokenRequestDto; +import rootbox.rootboxApp.global.feign.dto.OAuthInfoDto; +import rootbox.rootboxApp.global.feign.mapper.KakaoOAuthMapper; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class KakaoOauthService { + + private final KakaoInfoFeignClient kakaoInfoFeignClient; + + private final KakaotestFeignClient kakaotestFeignClient; + + @Value("${oauth.kakao.redirectUrl}") + private String redirectUrl; + + @Value("${oauth.kakao.clientId}") + private String clientId; + + + public OAuthInfoDto getKakaoUserInfo(String token) { + KakaoSocialUserDto info = kakaoInfoFeignClient.getInfo(token); + return KakaoOAuthMapper.toOAuthInfoDto(info); + } + + public void getKakaoCode(){ + kakaotestFeignClient.getCode("code",clientId,redirectUrl); + } + + public String getKakaoCodeUrl(){ + String baseUrl = "https://kauth.kakao.com/oauth/authorize"; + return baseUrl + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUrl; + } + + public String getKakaoAccessToken(String code){ + Map params = Map.of( + "grant_type", "authorization_code", + "client_id", clientId, + "redirect_uri", redirectUrl, + "code", code + ); + + return KakaoOAuthMapper.toKakaoToken(kakaotestFeignClient.getToken(params)); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java index e9710f7..8617539 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java +++ b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java @@ -54,6 +54,7 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/v3/api-docs/**", "/favicon.io", "/swagger-ui/**", + "/swagger-ui.html", "/docs/**"); } @@ -70,7 +71,7 @@ public SecurityFilterChain JwtFilterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests( authorize -> { // authorize.requestMatchers("/swagger-ui/**").permitAll(); - authorize.requestMatchers("/api/v1/users/**").permitAll(); + authorize.requestMatchers("/api/v1/users/auth/**").permitAll(); authorize.anyRequest().authenticated(); }) .exceptionHandling( diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 88bb5f5..ffa8a75 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -78,6 +78,14 @@ jwt: firebase: admin-sdk: ${FCM_KEY} +oauth: + kakao: + baseUrl: ${KAKAO_BASE_URL} + clientId: ${KAKAO_CLIENT_ID} + redirectUrl : ${KAKAO_REDIRECT_URL} + secretKeyREST: ${KAKAO_SECRET_REST} + secretKeyAndroid : ${KAKAO_SECRET_ANDROID} + #cloud: # aws: # lambda: @@ -138,6 +146,12 @@ jwt: authorities-key: authoritiesKey access-token-validity-in-seconds: 120000 # 2 min refresh-token-validity-in-seconds: 300000 # 5 min +oauth: + kakao: + baseUrl: ${KAKAO_BASE_URL} + clientId: ${KAKAO_REST_KEY} + redirectUrl : ${KAKAO_REDIRECT_URL} + secretKeyREST: ${KAKAO_SECRET_REST} #firebase: # admin-sdk: ${FCM_KEY} From e5935a234a84eb9adff95ca7acb92878716348af Mon Sep 17 00:00:00 2001 From: CYY1007 Date: Sun, 6 Jul 2025 14:49:13 +0900 Subject: [PATCH 2/2] . --- .../rootboxApp/api/user/presentation/UserApi.java | 12 +++++++----- .../global/security/config/SecurityConfig.java | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java index 8ec23c5..0e5d0b6 100644 --- a/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java +++ b/src/main/java/rootbox/rootboxApp/api/user/presentation/UserApi.java @@ -18,24 +18,26 @@ @Slf4j @Validated @Tag(name = "User Api", description = "rootbox 사용자 관련 Api입니다.") -@RequestMapping(value = "/api/v1/users") public class UserApi { private final UserService userService; - @GetMapping(value = "/auth/health") + @GetMapping(value = "/api/v1/user/health") + public String health2() {return "I'm healthy!!!" ;} + + @GetMapping(value = "/api/v1/users/auth/health") public String health() {return "I'm healthy!!!" ;} - @PostMapping(value = "/auth/kakao") + @PostMapping(value = "/api/v1/users/auth/kakao") public CommonResponse kakaoSocialLogin(@RequestBody @Valid SocialLoginDto.KakaoSocialLoginRequestDto requestDto) { return CommonResponse.onSuccess(userService.socialLogin(requestDto)); } - @GetMapping(value = "/auth/kakao/code") + @GetMapping(value = "/api/v1/users/auth/kakao/code") public void kakaoSocailLoginTest(HttpServletResponse response) throws IOException { response.sendRedirect(userService.getKakaoCode()); } - @GetMapping(value = "/auth/kakao/test") + @GetMapping(value = "/api/v1/users/auth/kakao/test") public CommonResponse getKakaoToken(@RequestParam("code") String code){ return CommonResponse.onSuccess(userService.getKakaoToken(code)); } diff --git a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java index 8617539..cb148b5 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java +++ b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java @@ -72,6 +72,7 @@ public SecurityFilterChain JwtFilterChain(HttpSecurity http) throws Exception { authorize -> { // authorize.requestMatchers("/swagger-ui/**").permitAll(); authorize.requestMatchers("/api/v1/users/auth/**").permitAll(); + authorize.requestMatchers("/api/v1/user/**").permitAll(); authorize.anyRequest().authenticated(); }) .exceptionHandling(